Skip to content

Commit

Permalink
Merge pull request #388 from brendandburns/oidc
Browse files Browse the repository at this point in the history
Improve the OIDC auth to pull the expiration date from the token.
  • Loading branch information
k8s-ci-robot authored Jan 11, 2020
2 parents aa0b274 + 701b524 commit 632b3f9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 15 deletions.
7 changes: 6 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 50 additions & 10 deletions src/oidc_auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
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';

interface JwtObj {
header: any;
payload: any;
signature: string;
}

export class OpenIDConnectAuth implements Authenticator {
public static decodeJWT(token: string): JwtObj | null {
const parts = token.split('.');
if (parts.length !== 3) {
return null;
}

const header = JSON.parse(new TextDecoder().decode(base64url.parse(parts[0], { loose: true })));
const payload = JSON.parse(new TextDecoder().decode(base64url.parse(parts[1], { loose: true })));
const signature = parts[2];

return {
header,
payload,
signature,
};
}

public static expirationFromToken(token: string): number {
const jwt = OpenIDConnectAuth.decodeJWT(token);
if (!jwt) {
return 0;
}
return jwt.payload.exp;
}

// public for testing purposes.
private currentTokenExpiration = 0;
public isAuthProvider(user: User): boolean {
Expand Down Expand Up @@ -39,21 +72,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<string> {
private async refresh(user: User, overrideClient?: Client): Promise<string | null> {
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;
Expand Down
64 changes: 60 additions & 4 deletions src/oidc_auth_test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
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 a JWT', () => {
const jwt = OpenIDConnectAuth.decodeJWT(
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.5mhBHqs5_DTLdINd9p5m7ZJ6XD0Xc55kIaCRY5r6HRA',
);
expect(jwt).to.not.be.null;
});

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: {
Expand Down Expand Up @@ -52,11 +81,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/',
Expand Down Expand Up @@ -91,11 +122,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/',
Expand All @@ -109,12 +142,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',
Expand Down

0 comments on commit 632b3f9

Please sign in to comment.