Skip to content

Commit 632b3f9

Browse files
authored
Merge pull request #388 from brendandburns/oidc
Improve the OIDC auth to pull the expiration date from the token.
2 parents aa0b274 + 701b524 commit 632b3f9

File tree

4 files changed

+117
-15
lines changed

4 files changed

+117
-15
lines changed

package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"jsonpath-plus": "^0.19.0",
6666
"openid-client": "2.5.0",
6767
"request": "^2.88.0",
68+
"rfc4648": "^1.3.0",
6869
"shelljs": "^0.8.2",
6970
"tslib": "^1.9.3",
7071
"underscore": "^1.9.1",

src/oidc_auth.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
11
import https = require('https');
22
import { Client, Issuer } from 'openid-client';
33
import request = require('request');
4+
import { base64url } from 'rfc4648';
5+
import { TextDecoder } from 'util';
46

57
import { Authenticator } from './auth';
68
import { User } from './config_types';
79

10+
interface JwtObj {
11+
header: any;
12+
payload: any;
13+
signature: string;
14+
}
15+
816
export class OpenIDConnectAuth implements Authenticator {
17+
public static decodeJWT(token: string): JwtObj | null {
18+
const parts = token.split('.');
19+
if (parts.length !== 3) {
20+
return null;
21+
}
22+
23+
const header = JSON.parse(new TextDecoder().decode(base64url.parse(parts[0], { loose: true })));
24+
const payload = JSON.parse(new TextDecoder().decode(base64url.parse(parts[1], { loose: true })));
25+
const signature = parts[2];
26+
27+
return {
28+
header,
29+
payload,
30+
signature,
31+
};
32+
}
33+
34+
public static expirationFromToken(token: string): number {
35+
const jwt = OpenIDConnectAuth.decodeJWT(token);
36+
if (!jwt) {
37+
return 0;
38+
}
39+
return jwt.payload.exp;
40+
}
41+
942
// public for testing purposes.
1043
private currentTokenExpiration = 0;
1144
public isAuthProvider(user: User): boolean {
@@ -39,21 +72,28 @@ export class OpenIDConnectAuth implements Authenticator {
3972
if (!user.authProvider.config['client-secret']) {
4073
user.authProvider.config['client-secret'] = '';
4174
}
42-
if (
43-
!user.authProvider.config ||
44-
!user.authProvider.config['id-token'] ||
45-
!user.authProvider.config['client-id'] ||
46-
!user.authProvider.config['refresh-token'] ||
47-
!user.authProvider.config['idp-issuer-url']
48-
) {
75+
if (!user.authProvider.config || !user.authProvider.config['id-token']) {
4976
return null;
5077
}
51-
const client = overrideClient ? overrideClient : await this.getClient(user);
52-
return this.refresh(user, client);
78+
return this.refresh(user, overrideClient);
5379
}
5480

55-
private async refresh(user: User, client: Client): Promise<string> {
81+
private async refresh(user: User, overrideClient?: Client): Promise<string | null> {
82+
if (this.currentTokenExpiration === 0) {
83+
this.currentTokenExpiration = OpenIDConnectAuth.expirationFromToken(
84+
user.authProvider.config['id-token'],
85+
);
86+
}
5687
if (Date.now() / 1000 > this.currentTokenExpiration) {
88+
if (
89+
!user.authProvider.config['client-id'] ||
90+
!user.authProvider.config['refresh-token'] ||
91+
!user.authProvider.config['idp-issuer-url']
92+
) {
93+
return null;
94+
}
95+
96+
const client = overrideClient ? overrideClient : await this.getClient(user);
5797
const newToken = await client.refresh(user.authProvider.config['refresh-token']);
5898
user.authProvider.config['id-token'] = newToken.id_token;
5999
user.authProvider.config['refresh-token'] = newToken.refresh_token;

src/oidc_auth_test.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
11
import { expect } from 'chai';
22
import * as request from 'request';
3+
import { base64url } from 'rfc4648';
4+
import { TextEncoder } from 'util';
35

46
import { User } from './config_types';
57
import { OpenIDConnectAuth } from './oidc_auth';
68

9+
function encode(value: string): string {
10+
return base64url.stringify(new TextEncoder().encode(value));
11+
}
12+
13+
function makeJWT(header: string, payload: object, signature: string): string {
14+
return encode(header) + '.' + encode(JSON.stringify(payload)) + '.' + encode(signature);
15+
}
16+
717
describe('OIDCAuth', () => {
8-
const auth = new OpenIDConnectAuth();
18+
var auth: OpenIDConnectAuth;
19+
beforeEach(() => {
20+
auth = new OpenIDConnectAuth();
21+
});
22+
23+
it('should correctly parse a JWT', () => {
24+
const jwt = OpenIDConnectAuth.decodeJWT(
25+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.5mhBHqs5_DTLdINd9p5m7ZJ6XD0Xc55kIaCRY5r6HRA',
26+
);
27+
expect(jwt).to.not.be.null;
28+
});
29+
30+
it('should correctly parse time from token', () => {
31+
const time = Math.floor(Date.now() / 1000);
32+
const token = makeJWT('{}', { exp: time }, 'fake');
33+
const timeOut = OpenIDConnectAuth.expirationFromToken(token);
34+
35+
expect(timeOut).to.equal(time);
36+
});
37+
938
it('should be true for oidc user', () => {
1039
const user = {
1140
authProvider: {
@@ -52,11 +81,13 @@ describe('OIDCAuth', () => {
5281
});
5382

5483
it('authorization should be undefined if client-id missing', async () => {
84+
const past = 100;
85+
const token = makeJWT('{}', { exp: past }, 'fake');
5586
const user = {
5687
authProvider: {
5788
name: 'oidc',
5889
config: {
59-
'id-token': 'fakeToken',
90+
'id-token': token,
6091
'client-secret': 'clientsecret',
6192
'refresh-token': 'refreshtoken',
6293
'idp-issuer-url': 'https://www.google.com/',
@@ -91,11 +122,13 @@ describe('OIDCAuth', () => {
91122
});
92123

93124
it('authorization should be undefined if refresh-token missing', async () => {
125+
const past = 100;
126+
const token = makeJWT('{}', { exp: past }, 'fake');
94127
const user = {
95128
authProvider: {
96129
name: 'oidc',
97130
config: {
98-
'id-token': 'fakeToken',
131+
'id-token': token,
99132
'client-id': 'id',
100133
'client-secret': 'clientsecret',
101134
'idp-issuer-url': 'https://www.google.com/',
@@ -109,12 +142,35 @@ describe('OIDCAuth', () => {
109142
expect(opts.headers.Authorization).to.be.undefined;
110143
});
111144

145+
it('authorization should work if refresh-token missing but token is unexpired', async () => {
146+
const future = Date.now() / 1000 + 1000000;
147+
const token = makeJWT('{}', { exp: future }, 'fake');
148+
const user = {
149+
authProvider: {
150+
name: 'oidc',
151+
config: {
152+
'id-token': token,
153+
'client-id': 'id',
154+
'client-secret': 'clientsecret',
155+
'idp-issuer-url': 'https://www.google.com/',
156+
},
157+
},
158+
} as User;
159+
160+
const opts = {} as request.Options;
161+
opts.headers = [];
162+
await auth.applyAuthentication(user, opts);
163+
expect(opts.headers.Authorization).to.equal(`Bearer ${token}`);
164+
});
165+
112166
it('authorization should be undefined if idp-issuer-url missing', async () => {
167+
const past = 100;
168+
const token = makeJWT('{}', { exp: past }, 'fake');
113169
const user = {
114170
authProvider: {
115171
name: 'oidc',
116172
config: {
117-
'id-token': 'fakeToken',
173+
'id-token': token,
118174
'client-id': 'id',
119175
'client-secret': 'clientsecret',
120176
'refresh-token': 'refreshtoken',

0 commit comments

Comments
 (0)