diff --git a/packages/okta-vue/CHANGELOG.md b/packages/okta-vue/CHANGELOG.md index 11899fc1..df6da8a9 100644 --- a/packages/okta-vue/CHANGELOG.md +++ b/packages/okta-vue/CHANGELOG.md @@ -1,3 +1,16 @@ +# 2.0.0 + +### Breaking Changes + +- Uses/requires `@okta/okta-auth-js 3.x` + - The `pkce` option now defaults to `true`, using the Authorization Code w/PKCE flow + - Those using the (previous default) Implicit Flow should pass `pkce: false` to their config + - See the [@okta/okta-auth-js README regarding PKCE OAuth2 Flow](https://github.com/okta/okta-auth-js#pkce-oauth-20-flow) for PKCE requirements + - Which include the Application settings in the Okta Admin Dashboard allowing for PKCE +- The previously deprecated `scope` option is now fully unsupported +- The `scopes` option now defaults to `['openid', 'email', 'profile']` instead of the previous `['openid']` + - This default continues to be overridden by any explicit `scopes` passed in the config + # 1.3.0 ### Features diff --git a/packages/okta-vue/README.md b/packages/okta-vue/README.md index 77c5cbf7..43fcf381 100644 --- a/packages/okta-vue/README.md +++ b/packages/okta-vue/README.md @@ -236,22 +236,18 @@ The most commonly used options are shown here. See [Configuration Reference](htt - `clientId` **(required)**: The OpenID Connect `client_id` - `redirectUri` **(required)**: Where the callback is hosted - `postLogoutRedirectUri` | Specify the url where the browser should be redirected after [logout](#authlogouturi). This url must be added to the list of `Logout redirect URIs` on the application's `General Settings` tab. -- `scope` *(deprecated in v1.1.1)*: Use `scopes` instead -- `scopes` *(optional)*: Reserved or custom claims to be returned in the tokens. Defaults to `openid`, which will only return the `sub` claim. To obtain more information about the user, use `openid profile`. For a list of scopes and claims, please see [Scope-dependent claims](https://developer.okta.com/standards/OIDC/index.html#scope-dependent-claims-not-always-returned) for more information. +- `scopes` *(optional)*: Reserved or custom claims to be returned in the tokens. Defaults to `['openid', 'email', 'profile']`. For a list of scopes and claims, please see [Scope-dependent claims](https://developer.okta.com/standards/OIDC/index.html#scope-dependent-claims-not-always-returned) for more information. - `responseType` *(optional)*: Desired token grant types. Default: `['id_token', 'token']`. For PKCE flow, this should be left undefined or set to `['code']`. -- `pkce` *(optional)* - If `true`, PKCE flow will be used +- `pkce` *(optional)* - If `true`, Authorization Code w/PKCE flow will be used. Defaults to `true` - `onAuthRequired` *(optional)*: - callback function. Called when authentication is required. If not supplied, `okta-vue` will redirect directly to Okta for authentication. This is triggered when: 1. [login](#authloginfromuri-additionalparams) is called 2. A route protected by `$auth.authRedirectGuard` is accessed without authentication - `onSessionExpired` *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK adds a default handler which will call [login](#authloginfromuri-additionalparams) to initiate a login flow. Passing a function here will disable the default handler. - `isAuthenticated` *(optional)* - callback function. By default, [$auth.isAuthenticated](#authisauthenticated) will return true if both `getIdToken()` and `getAccessToken()` return a value. Setting a `isAuthenticated` function on the config will skip the default logic and call the supplied function instead. The function should return a Promise and resolve to either true or false. - `tokenManager` *(optional)*: An object containing additional properties used to configure the internal token manager. See [AuthJS TokenManager](https://github.com/okta/okta-auth-js#the-tokenmanager) for more detailed information. - - `autoRenew` *(optional)*: By default, the library will attempt to renew expired tokens. When an expired token is requested by the library, a renewal request is executed to update the token. If you wish to to disable auto renewal of tokens, set autoRenew to false. - - `secure`: If `true` then only "secure" https cookies will be stored. This option will prevent cookies from being stored on an HTTP connection. This option is only relevant if `storage` is set to `cookie`, or if the client browser does not support `localStorage` or `sessionStorage`, in which case `cookie` storage will be used. - - `storage` *(optional)*: Specify the type of storage for tokens. The types are: diff --git a/packages/okta-vue/package.json b/packages/okta-vue/package.json index b9444d91..807f69e5 100644 --- a/packages/okta-vue/package.json +++ b/packages/okta-vue/package.json @@ -1,6 +1,6 @@ { "name": "@okta/okta-vue", - "version": "1.3.0", + "version": "2.0.0", "description": "Vue support for Okta", "main": "dist/okta-vue.js", "files": [ @@ -52,7 +52,7 @@ "homepage": "https://github.com/okta/okta-oidc-js#readme", "dependencies": { "@okta/configuration-validation": "^0.4.1", - "@okta/okta-auth-js": "^2.11.2", + "@okta/okta-auth-js": "^3.0.1", "cross-env": "^5.1.1", "vue": "^2.5.17", "vue-router": "^3.0.1" diff --git a/packages/okta-vue/src/Auth.js b/packages/okta-vue/src/Auth.js index bd4ba492..7f5dff71 100644 --- a/packages/okta-vue/src/Auth.js +++ b/packages/okta-vue/src/Auth.js @@ -53,11 +53,14 @@ export default class Auth { } async handleAuthentication () { - const tokens = await this.oktaAuth.token.parseFromUrl() - tokens.forEach(token => { - if (token.accessToken) this.oktaAuth.tokenManager.add('accessToken', token) - if (token.idToken) this.oktaAuth.tokenManager.add('idToken', token) - }) + const {tokens} = await this.oktaAuth.token.parseFromUrl() + + if (tokens.idToken) { + this.oktaAuth.tokenManager.add('idToken', tokens.idToken) + } + if (tokens.accessToken) { + this.oktaAuth.tokenManager.add('accessToken', tokens.accessToken) + } } setFromUri (fromUri) { @@ -97,15 +100,12 @@ export default class Auth { async getUser () { const accessToken = await this.oktaAuth.tokenManager.get('accessToken') const idToken = await this.oktaAuth.tokenManager.get('idToken') - if (accessToken && idToken) { - const userinfo = await this.oktaAuth.token.getUserInfo(accessToken) - if (userinfo.sub === idToken.claims.sub) { - // Only return the userinfo response if subjects match to - // mitigate token substitution attacks - return userinfo - } + + if (!accessToken || !idToken) { + return idToken ? idToken.claims : undefined } - return idToken ? idToken.claims : undefined + + return this.oktaAuth.token.getUserInfo() } authRedirectGuard () { diff --git a/packages/okta-vue/src/config.js b/packages/okta-vue/src/config.js index 8c7ead1f..0856d6a5 100644 --- a/packages/okta-vue/src/config.js +++ b/packages/okta-vue/src/config.js @@ -14,9 +14,10 @@ export default function initConfig (options) { assertClientId(auth.clientId) assertRedirectUri(auth.redirectUri) - // Ensure "openid" exists in the scopes - auth.scopes = auth.scopes || [] - if (auth.scopes.indexOf('openid') < 0) { + // Default scopes, override as needed + auth.scopes = auth.scopes || ['openid', 'email', 'profile'] + // Force 'openid' as a scope + if (!auth.scopes.includes('openid')) { auth.scopes.unshift('openid') } diff --git a/packages/okta-vue/test/jest/Auth.interface.spec.js b/packages/okta-vue/test/jest/Auth.interface.spec.js index 44b080c2..cf348346 100644 --- a/packages/okta-vue/test/jest/Auth.interface.spec.js +++ b/packages/okta-vue/test/jest/Auth.interface.spec.js @@ -41,7 +41,7 @@ describe('Auth constructor', () => { }) }) - test('sets the right user agent on AuthJS', () => { + it('sets the right user agent on AuthJS', () => { const expectedUserAgent = `${pkg.name}/${pkg.version} foo` createAuth() expect(mockAuthJsInstance.userAgent).toMatch(expectedUserAgent) @@ -50,36 +50,18 @@ describe('Auth constructor', () => { it('sets the right scope and response_type when constructing AuthJS instance', () => { createAuth() expect(AuthJS).toHaveBeenCalledWith(Object.assign({}, baseConfig, { - scopes: ['openid'], + scopes: ['openid', 'email', 'profile'], responseType: ['id_token', 'token'], onSessionExpired: expect.any(Function) })) }) - test('sets the right scope and response_type overrides (legacy config)', async () => { - const legacyConfig = { - issuer: 'https://foo', - client_id: 'foo', - redirect_uri: 'foo', - scope: 'foo bar', - response_type: 'token foo' - } - createAuth(legacyConfig) - expect(AuthJS).toHaveBeenCalledWith(Object.assign({}, legacyConfig, { - clientId: 'foo', - redirectUri: 'foo', - scopes: ['openid', 'foo', 'bar'], - responseType: ['token', 'foo'], - onSessionExpired: expect.any(Function) - })) - }) - it('will not overwrite responseType if set', () => { createAuth(extendConfig({ responseType: ['fake'] })) expect(AuthJS).toHaveBeenCalledWith(Object.assign({}, baseConfig, { - scopes: ['openid'], + scopes: ['openid', 'email', 'profile'], responseType: ['fake'], onSessionExpired: expect.any(Function) })) @@ -246,25 +228,25 @@ describe('logout', () => { auth = createAuth() }) - test('calls "signOut', async () => { + it('calls "signOut', async () => { await auth.logout() expect(mockAuthJsInstance.signOut).toHaveBeenCalled() }) - test('passes options', async () => { + it('passes options', async () => { const options = { foo: 'bar' } await auth.logout(options) expect(mockAuthJsInstance.signOut).toHaveBeenCalledWith(options) }) - test('returns a promise', async () => { + it('returns a promise', async () => { const res = auth.logout() expect(typeof res.then).toBe('function') expect(typeof res.catch).toBe('function') return res }) - test('can throw', async () => { + it('can throw', async () => { const testError = new Error('test error') signoutRes = Promise.reject(testError) return auth.logout() @@ -285,7 +267,7 @@ describe('isAuthenticated', () => { }) auth = createAuth(extendConfig(config)) } - test('isAuthenticated() returns false when the TokenManager throws an error', async () => { + it('isAuthenticated() returns false when the TokenManager throws an error', async () => { bootstrap() mockAuthJsInstance.tokenManager = { get: jest.fn().mockImplementation(() => { @@ -297,7 +279,7 @@ describe('isAuthenticated', () => { expect(authenticated).toBeFalsy() }) - test('isAuthenticated() returns false when the TokenManager does not return an access token', async () => { + it('isAuthenticated() returns false when the TokenManager does not return an access token', async () => { bootstrap() mockAuthJsInstance.tokenManager = { get: jest.fn().mockImplementation(() => { @@ -308,7 +290,7 @@ describe('isAuthenticated', () => { expect(authenticated).toBeFalsy() }) - test('isAuthenticated() returns true when the TokenManager returns an access token', async () => { + it('isAuthenticated() returns true when the TokenManager returns an access token', async () => { bootstrap() mockAuthJsInstance.tokenManager = { get: jest.fn().mockReturnValue(Promise.resolve({ accessToken: 'fake' })) @@ -338,7 +320,7 @@ describe('handleAuthentication', () => { function bootstrap (tokens) { mockAuthJsInstance = extendMockAuthJS({ token: { - parseFromUrl: jest.fn().mockReturnValue(Promise.resolve(tokens)) + parseFromUrl: jest.fn().mockReturnValue(Promise.resolve({ tokens })) }, tokenManager: { add: jest.fn() @@ -353,18 +335,18 @@ describe('handleAuthentication', () => { it('stores accessToken and idToken', async () => { var accessToken = { accessToken: 'X' } var idToken = { idToken: 'Y' } - bootstrap([ + bootstrap({ accessToken, idToken - ]) + }) await auth.handleAuthentication() - expect(mockAuthJsInstance.tokenManager.add).toHaveBeenNthCalledWith(1, 'accessToken', accessToken) - expect(mockAuthJsInstance.tokenManager.add).toHaveBeenNthCalledWith(2, 'idToken', idToken) + expect(mockAuthJsInstance.tokenManager.add).toHaveBeenNthCalledWith(1, 'idToken', idToken) + expect(mockAuthJsInstance.tokenManager.add).toHaveBeenNthCalledWith(2, 'accessToken', accessToken) }) }) describe('setFromUri', () => { - test('sets referrer in localStorage', () => { + it('sets referrer in localStorage', () => { const TEST_VALUE = 'foo-bar' localStorage.setItem('referrerPath', '') const auth = createAuth() @@ -374,7 +356,7 @@ describe('setFromUri', () => { }) describe('getFromUri', () => { - test('cleares referrer from localStorage', () => { + it('cleares referrer from localStorage', () => { const TEST_VALUE = 'foo-bar' localStorage.setItem('referrerPath', TEST_VALUE) const auth = createAuth() @@ -399,7 +381,7 @@ describe('getAccessToken', () => { auth = createAuth() } - test('can retrieve an accessToken from the tokenManager', async () => { + it('can retrieve an accessToken from the tokenManager', async () => { const accessToken = { accessToken: 'fake' } bootstrap(accessToken) const val = await auth.getAccessToken() @@ -423,7 +405,7 @@ describe('getIdToken', () => { auth = createAuth() } - test('can retrieve an idToken from the tokenManager', async () => { + it('can retrieve an idToken from the tokenManager', async () => { const idToken = { idToken: 'fake' } bootstrap(idToken) const val = await auth.getIdToken() @@ -456,13 +438,13 @@ describe('getUser', () => { auth = createAuth() } - test('no tokens: returns undefined', async () => { + it('no tokens: returns undefined', async () => { bootstrap() const val = await auth.getUser() expect(val).toBe(undefined) }) - test('idToken only: returns claims', async () => { + it('idToken only: returns claims', async () => { const claims = { foo: 'bar' } bootstrap({ idToken: { claims } @@ -471,7 +453,7 @@ describe('getUser', () => { expect(val).toBe(claims) }) - test('idToken and accessToken: calls getUserInfo', async () => { + it('idToken and accessToken: calls getUserInfo', async () => { bootstrap({ accessToken: {}, idToken: { claims: {} }, @@ -480,32 +462,6 @@ describe('getUser', () => { await auth.getUser() expect(mockAuthJsInstance.token.getUserInfo).toHaveBeenCalled() }) - - test('idToken and accessToken: matching sub returns userInfo', async () => { - const sub = 'fake' - const userInfo = { sub } - const claims = { sub } - bootstrap({ - accessToken: {}, - idToken: { claims }, - userInfo - }) - const val = await auth.getUser() - expect(val).toBe(userInfo) - }) - - test('idToken and accessToken: mis-matching sub returns claims', async () => { - const sub = 'fake' - const userInfo = { sub: 'not-fake?' } - const claims = { sub } - bootstrap({ - accessToken: {}, - idToken: { claims }, - userInfo - }) - const val = await auth.getUser() - expect(val).toBe(claims) - }) }) describe('TokenManager', () => { diff --git a/packages/okta-vue/yarn.lock b/packages/okta-vue/yarn.lock index 89e9e1bc..09015802 100644 --- a/packages/okta-vue/yarn.lock +++ b/packages/okta-vue/yarn.lock @@ -20,16 +20,15 @@ version "0.4.1" resolved "https://registry.yarnpkg.com/@okta/configuration-validation/-/configuration-validation-0.4.1.tgz#6fa4520bc96c27b3d7aedcb0523de1fbceee9105" -"@okta/okta-auth-js@^2.11.2": - version "2.11.2" - resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-2.11.2.tgz#d2d867e45eb98d453f156ec68c927bf1594277f6" +"@okta/okta-auth-js@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-3.0.1.tgz#90ed51ec194845595156d8260414950ee071cba9" + integrity sha512-g36mvt5q9NScY1ljFHpahGzkTbWL0FzWM8vrNcJHMGk1ep7/5Y9eYqq5OF99QVLeJI3APsw8FneyXneCSeF7sw== dependencies: Base64 "0.3.0" cross-fetch "^3.0.0" js-cookie "2.2.0" node-cache "^4.2.0" - q "1.4.1" - reqwest "2.0.5" tiny-emitter "1.1.0" xhr2 "0.1.3" @@ -5128,10 +5127,6 @@ punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" -q@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" - qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -5402,10 +5397,6 @@ require-uncached@^1.0.2: caller-path "^0.1.0" resolve-from "^1.0.0" -reqwest@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/reqwest/-/reqwest-2.0.5.tgz#00fb15ac4918c419ca82b43f24c78882e66039a1" - resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"