-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[commerce-sdk-react] Decode pre-fetched token and save auth data in storage #1052
Changes from all commits
1adfbb2
45f74e9
727588b
576aeaf
2b6d3d9
83e62be
2ea4914
6d852a5
8a8f49b
17807b6
a4067b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -190,11 +190,52 @@ describe('Auth', () => { | |
expect(auth.ready()).resolves.toEqual(result) | ||
}) | ||
test('ready - use `fetchedToken` and short circuit network request', async () => { | ||
const auth = new Auth({...config, fetchedToken: 'fake-token'}) | ||
const fetchedToken = jwt.sign( | ||
{ | ||
sub: `cc-slas::zzrf_001::scid:xxxxxx::usid:usid`, | ||
isb: `uido:ecom::upn:[email protected]::uidn:firstname lastname::gcid:guestuserid::rcid:rcid::chid:siteId` | ||
}, | ||
'secret' | ||
) | ||
const auth = new Auth({...config, fetchedToken}) | ||
jest.spyOn(auth, 'queueRequest') | ||
await auth.ready().then(() => { | ||
expect(auth.queueRequest).not.toHaveBeenCalled() | ||
}) | ||
await auth.ready() | ||
expect(auth.queueRequest).not.toHaveBeenCalled() | ||
}) | ||
test('ready - use `fetchedToken` and auth data is populated for registered user', async () => { | ||
const usid = 'usidddddd' | ||
const customerId = 'customerIddddddd' | ||
const fetchedToken = jwt.sign( | ||
{ | ||
sub: `cc-slas::zzrf_001::scid:xxxxxx::usid:${usid}`, | ||
isb: `uido:ecom::upn:[email protected]::uidn:firstname lastname::gcid:guestuserid::rcid:${customerId}::chid:siteId` | ||
}, | ||
'secret' | ||
) | ||
const auth = new Auth({...config, fetchedToken}) | ||
await auth.ready() | ||
expect(auth.get('access_token')).toBe(fetchedToken) | ||
expect(auth.get('customer_id')).toBe(customerId) | ||
expect(auth.get('usid')).toBe(usid) | ||
expect(auth.get('customer_type')).toBe('registered') | ||
}) | ||
test('ready - use `fetchedToken` and auth data is populated for guest user', async () => { | ||
// isb: `uido:slas::upn:Guest::uidn:Guest User::gcid:bclrdGlbIZlHaRxHsZlWYYxHwZ::chid: ` | ||
const usid = 'usidddddd' | ||
const customerId = 'customerIddddddd' | ||
const fetchedToken = jwt.sign( | ||
{ | ||
sub: `cc-slas::zzrf_001::scid:xxxxxx::usid:${usid}`, | ||
isb: `uido:ecom::upn:Guest::uidn:firstname lastname::gcid:${customerId}::rcid:registeredCid::chid:siteId` | ||
}, | ||
'secret' | ||
) | ||
const auth = new Auth({...config, fetchedToken}) | ||
await auth.ready() | ||
expect(auth.get('access_token')).toBe(fetchedToken) | ||
expect(auth.get('customer_id')).toBe(customerId) | ||
expect(auth.get('usid')).toBe(usid) | ||
expect(auth.get('customer_type')).toBe('guest') | ||
}) | ||
test('ready - use refresh token when access token is expired', async () => { | ||
const auth = new Auth(config) | ||
|
@@ -220,34 +261,29 @@ describe('Auth', () => { | |
auth.set(key, data[key]) | ||
}) | ||
|
||
await auth.ready().then(() => { | ||
expect(helpers.refreshAccessToken).toBeCalled() | ||
}) | ||
await auth.ready() | ||
expect(helpers.refreshAccessToken).toBeCalled() | ||
}) | ||
test('ready - PKCE flow', async () => { | ||
const auth = new Auth(config) | ||
|
||
await auth.ready().then(() => { | ||
expect(helpers.loginGuestUser).toBeCalled() | ||
}) | ||
await auth.ready() | ||
expect(helpers.loginGuestUser).toBeCalled() | ||
}) | ||
test('loginGuestUser', async () => { | ||
const auth = new Auth(config) | ||
await auth.loginGuestUser().then(() => { | ||
expect(helpers.loginGuestUser).toBeCalled() | ||
}) | ||
await auth.loginGuestUser() | ||
expect(helpers.loginGuestUser).toBeCalled() | ||
}) | ||
test('loginRegisteredUserB2C', async () => { | ||
const auth = new Auth(config) | ||
await auth.loginRegisteredUserB2C({username: 'test', password: 'test'}).then(() => { | ||
expect(helpers.loginRegisteredUserB2C).toBeCalled() | ||
}) | ||
await auth.loginRegisteredUserB2C({username: 'test', password: 'test'}) | ||
expect(helpers.loginRegisteredUserB2C).toBeCalled() | ||
}) | ||
test('logout', async () => { | ||
const auth = new Auth(config) | ||
await auth.logout().then(() => { | ||
expect(helpers.loginGuestUser).toBeCalled() | ||
}) | ||
await auth.logout() | ||
expect(helpers.loginGuestUser).toBeCalled() | ||
}) | ||
test('running on the server uses a shared context memory store', async () => { | ||
const refreshTokenGuest = 'guest' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ import { | |
ShopperLoginTypes, | ||
ShopperCustomersTypes | ||
} from 'commerce-sdk-isomorphic' | ||
import jwtDecode from 'jwt-decode' | ||
import jwtDecode, {JwtPayload} from 'jwt-decode' | ||
import {ApiClientConfigParams, Prettify, RemoveStringIndex} from '../hooks/types' | ||
import {BaseStorage, LocalStorage, CookieStorage, MemoryStorage, StorageType} from './storage' | ||
import {CustomerType} from '../hooks/useCustomerType' | ||
|
@@ -31,6 +31,11 @@ interface JWTHeaders { | |
iat: number | ||
} | ||
|
||
interface SlasJwtPayload extends JwtPayload { | ||
sub: string | ||
isb: string | ||
} | ||
|
||
/** | ||
* The extended field is not from api response, we manually store the auth type, | ||
* so we don't need to make another API call when we already have the data. | ||
|
@@ -288,7 +293,12 @@ class Auth { | |
*/ | ||
async ready() { | ||
if (this.fetchedToken && this.fetchedToken !== '') { | ||
this.pendingToken = Promise.resolve({...this.data, access_token: this.fetchedToken}) | ||
const {isGuest, customerId, usid} = this.parseSlasJWT(this.fetchedToken) | ||
this.set('access_token', this.fetchedToken) | ||
this.set('customer_id', customerId) | ||
this.set('usid', usid) | ||
this.set('customer_type', isGuest ? 'guest' : 'registered') | ||
this.pendingToken = Promise.resolve(this.data) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the difference between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return this.pendingToken | ||
} | ||
if (this.pendingToken) { | ||
|
@@ -405,6 +415,30 @@ class Auth { | |
this.clearStorage() | ||
return this.loginGuestUser() | ||
} | ||
|
||
/** | ||
* Decode SLAS JWT and extract information such as customer id, usid, etc. | ||
* | ||
*/ | ||
parseSlasJWT(jwt: string) { | ||
const payload = jwtDecode(jwt) as SlasJwtPayload | ||
const {sub, isb} = payload | ||
// ISB format | ||
// 'uido:ecom::upn:Guest||xxxEmailxxx::uidn:FirstName LastName::gcid:xxxGuestCustomerIdxxx::rcid:xxxRegisteredCustomerIdxxx::chid:xxxSiteIdxxx', | ||
const isbParts = isb.split('::') | ||
const isGuest = isbParts[1] === 'upn:Guest' | ||
const customerId = isGuest | ||
? isbParts[3].replace('gcid:', '') | ||
: isbParts[4].replace('rcid:', '') | ||
// SUB format | ||
// cc-slas::zzrf_001::scid:c9c45bfd-0ed3-4aa2-xxxx-40f88962b836::usid:b4865233-de92-4039-xxxx-aa2dfc8c1ea5 | ||
const usid = sub.split('::')[3].replace('usid:', '') | ||
return { | ||
isGuest, | ||
customerId, | ||
usid | ||
} | ||
} | ||
} | ||
|
||
export default Auth |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have a
fetchedToken
, we will re-parse all the data every time we call.ready()
. That doesn't seem like desired behavior. Not a big deal for our use case, testing, but we might want to avoid that if we want to make the feature usable for production use cases.