From 03b39dfce310d80a938773d04c14c843cf3deb7b Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Tue, 7 Jan 2020 21:46:58 -0800 Subject: [PATCH] Improve the OIDC auth to pull the expiration date from the token. --- package-lock.json | 7 +++++- package.json | 1 + src/oidc_auth.ts | 40 ++++++++++++++++++++++-------- src/oidc_auth_test.ts | 57 ++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0cd8aee1cd..eceb7473dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1167,7 +1167,7 @@ "handlebars": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", - "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -2708,6 +2708,11 @@ "lowercase-keys": "^1.0.0" } }, + "rfc4648": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.3.0.tgz", + "integrity": "sha512-x36K12jOflpm1V8QjPq3I+pt7Z1xzeZIjiC8J2Oxd7bE1efTrOG241DTYVJByP/SxR9jl1t7iZqYxDX864jgBQ==" + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", diff --git a/package.json b/package.json index 555da6f693..0b311e7feb 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "jsonpath-plus": "^0.19.0", "openid-client": "2.5.0", "request": "^2.88.0", + "rfc4648": "^1.3.0", "shelljs": "^0.8.2", "tslib": "^1.9.3", "underscore": "^1.9.1", diff --git a/src/oidc_auth.ts b/src/oidc_auth.ts index bd247e1852..46cb717361 100644 --- a/src/oidc_auth.ts +++ b/src/oidc_auth.ts @@ -1,11 +1,24 @@ import https = require('https'); import { Client, Issuer } from 'openid-client'; import request = require('request'); +import { base64url } from 'rfc4648'; +import { TextDecoder } from 'util'; import { Authenticator } from './auth'; import { User } from './config_types'; export class OpenIDConnectAuth implements Authenticator { + public static expirationFromToken(token: string): number { + const parts = token.split('.'); + if (parts.length !== 3) { + return 0; + } + + const payload = base64url.parse(parts[1]); + const claims = JSON.parse(new TextDecoder().decode(payload)); + return claims.exp; + } + // public for testing purposes. private currentTokenExpiration = 0; public isAuthProvider(user: User): boolean { @@ -39,21 +52,28 @@ export class OpenIDConnectAuth implements Authenticator { if (!user.authProvider.config['client-secret']) { user.authProvider.config['client-secret'] = ''; } - if ( - !user.authProvider.config || - !user.authProvider.config['id-token'] || - !user.authProvider.config['client-id'] || - !user.authProvider.config['refresh-token'] || - !user.authProvider.config['idp-issuer-url'] - ) { + if (!user.authProvider.config || !user.authProvider.config['id-token']) { return null; } - const client = overrideClient ? overrideClient : await this.getClient(user); - return this.refresh(user, client); + return this.refresh(user, overrideClient); } - private async refresh(user: User, client: Client): Promise { + private async refresh(user: User, overrideClient?: Client): Promise { + if (this.currentTokenExpiration === 0) { + this.currentTokenExpiration = OpenIDConnectAuth.expirationFromToken( + user.authProvider.config['id-token'], + ); + } if (Date.now() / 1000 > this.currentTokenExpiration) { + if ( + !user.authProvider.config['client-id'] || + !user.authProvider.config['refresh-token'] || + !user.authProvider.config['idp-issuer-url'] + ) { + return null; + } + + const client = overrideClient ? overrideClient : await this.getClient(user); const newToken = await client.refresh(user.authProvider.config['refresh-token']); user.authProvider.config['id-token'] = newToken.id_token; user.authProvider.config['refresh-token'] = newToken.refresh_token; diff --git a/src/oidc_auth_test.ts b/src/oidc_auth_test.ts index f21464f302..4b1f3ef85c 100644 --- a/src/oidc_auth_test.ts +++ b/src/oidc_auth_test.ts @@ -1,11 +1,33 @@ import { expect } from 'chai'; import * as request from 'request'; +import { base64url } from 'rfc4648'; +import { TextEncoder } from 'util'; import { User } from './config_types'; import { OpenIDConnectAuth } from './oidc_auth'; +function encode(value: string): string { + return base64url.stringify(new TextEncoder().encode(value)); +} + +function makeJWT(header: string, payload: object, signature: string): string { + return encode(header) + '.' + encode(JSON.stringify(payload)) + '.' + encode(signature); +} + describe('OIDCAuth', () => { - const auth = new OpenIDConnectAuth(); + var auth: OpenIDConnectAuth; + beforeEach(() => { + auth = new OpenIDConnectAuth(); + }); + + it('should correctly parse time from token', () => { + const time = Math.floor(Date.now() / 1000); + const token = makeJWT('{}', { exp: time }, 'fake'); + const timeOut = OpenIDConnectAuth.expirationFromToken(token); + + expect(timeOut).to.equal(time); + }); + it('should be true for oidc user', () => { const user = { authProvider: { @@ -52,11 +74,13 @@ describe('OIDCAuth', () => { }); it('authorization should be undefined if client-id missing', async () => { + const past = 100; + const token = makeJWT('{}', { exp: past }, 'fake'); const user = { authProvider: { name: 'oidc', config: { - 'id-token': 'fakeToken', + 'id-token': token, 'client-secret': 'clientsecret', 'refresh-token': 'refreshtoken', 'idp-issuer-url': 'https://www.google.com/', @@ -91,11 +115,13 @@ describe('OIDCAuth', () => { }); it('authorization should be undefined if refresh-token missing', async () => { + const past = 100; + const token = makeJWT('{}', { exp: past }, 'fake'); const user = { authProvider: { name: 'oidc', config: { - 'id-token': 'fakeToken', + 'id-token': token, 'client-id': 'id', 'client-secret': 'clientsecret', 'idp-issuer-url': 'https://www.google.com/', @@ -109,12 +135,35 @@ describe('OIDCAuth', () => { expect(opts.headers.Authorization).to.be.undefined; }); + it('authorization should work if refresh-token missing but token is unexpired', async () => { + const future = Date.now() / 1000 + 1000000; + const token = makeJWT('{}', { exp: future }, 'fake'); + const user = { + authProvider: { + name: 'oidc', + config: { + 'id-token': token, + 'client-id': 'id', + 'client-secret': 'clientsecret', + 'idp-issuer-url': 'https://www.google.com/', + }, + }, + } as User; + + const opts = {} as request.Options; + opts.headers = []; + await auth.applyAuthentication(user, opts); + expect(opts.headers.Authorization).to.equal(`Bearer ${token}`); + }); + it('authorization should be undefined if idp-issuer-url missing', async () => { + const past = 100; + const token = makeJWT('{}', { exp: past }, 'fake'); const user = { authProvider: { name: 'oidc', config: { - 'id-token': 'fakeToken', + 'id-token': token, 'client-id': 'id', 'client-secret': 'clientsecret', 'refresh-token': 'refreshtoken',