Skip to content

Commit

Permalink
feat: tokenset (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie authored Oct 11, 2021
1 parent 8758507 commit 1a919bc
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 7 deletions.
5 changes: 3 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"devDependencies": {
"@silverhand/eslint-config": "^0.2.2",
"@silverhand/essentials": "^1.1.0",
"@silverhand/ts-config": "^0.2.2",
"@types/jest": "^26.0.24",
"@types/jsonwebtoken": "^8.5.4",
Expand All @@ -31,7 +32,7 @@
"typescript": "^4.3.5"
},
"eslintConfig": {
"extends": "@logto"
"extends": "@silverhand/eslint-config"
},
"prettier": "@logto/eslint-config/.prettierrc"
"prettier": "@silverhand/eslint-config/.prettierrc"
}
96 changes: 96 additions & 0 deletions packages/client/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { randomBytes } from 'crypto';

import * as jwt from 'jsonwebtoken';

import { extractBearerToken, LogtoClient } from '.';

describe('extractBearerToken', () => {
Expand Down Expand Up @@ -45,3 +49,95 @@ describe('init client', () => {
expect(codeVerifier.length).toBeGreaterThan(10);
});
});

const generateRandomString = (length: number) =>
randomBytes(30)
.toString('hex')
.slice(0, length - 1);

const access_token = generateRandomString(43);
const refresh_token = generateRandomString(43);
const sub = generateRandomString(8);
const id_token = jwt.sign(
{
sub,
at_hash: 'RHPz55byGq-p81hfzGVYfA',
aud: 'foo',
exp: Math.floor(Date.now() / 1000) + 1000,
iat: Math.floor(Date.now() / 1000),
iss: 'https://logto.dev/oidc',
},
'secret'
);
const scope = 'openid offline_access';
const token_type = 'Bearer';

describe('setToken', () => {
let client: LogtoClient;
beforeAll((done) => {
client = new LogtoClient(
{
logtoUrl: 'https://logto.dev',
clientId: 'foo',
},
() => {
client.setToken({
access_token,
expires_at: Math.floor(Date.now() / 1000) + 1000,
id_token,
refresh_token,
scope,
token_type,
});
done();
}
);
});

test('should be authenticated', () => {
expect(client.isAuthenticated).toBeTruthy();
});

test('should have accessToken', () => {
expect(client.accessToken).toEqual(access_token);
});

test('should have id_token', () => {
expect(client.idToken).toEqual(id_token);
});

test('should have subject', () => {
expect(client.subject).toEqual(sub);
});
});

describe('setToken with expired input', () => {
let client: LogtoClient;
beforeAll((done) => {
client = new LogtoClient(
{
logtoUrl: 'https://logto.dev',
clientId: 'foo',
},
() => {
client.setToken({
access_token,
expires_at: Math.floor(Date.now() / 1000) - 1,
id_token,
refresh_token,
scope,
token_type,
});
done();
}
);
});

test('should not be authenticated', () => {
expect(client.isAuthenticated).toBeFalsy();
});

test('should throw on getting accessToken', () => {
expect(() => client.accessToken).toThrow();
});
});
45 changes: 40 additions & 5 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Client, Issuer, generators } from 'openid-client';
import { Client, Issuer, generators, TokenSet, TokenSetParameters } from 'openid-client';
import { Optional } from '@silverhand/essentials';

export interface ConfigParameters {
logtoUrl: string;
Expand Down Expand Up @@ -27,8 +28,9 @@ export const appendSlashIfNeeded = (url: string): string => {

export class LogtoClient {
public oidcReady = false;
public issuer: Issuer<Client> | null = null;
private client: Client | null = null;
public issuer: Optional<Issuer<Client>>;
private client: Optional<Client>;
private tokenSet: Optional<TokenSet>;
private readonly clientId: string;
constructor(config: ConfigParameters, onOidcReady?: () => void) {
const { logtoUrl, clientId } = config;
Expand All @@ -40,6 +42,34 @@ export class LogtoClient {
);
}

get isAuthenticated(): boolean {
return !this.tokenSet?.expired();
}

get accessToken(): string {
if (!this.isAuthenticated || !this.tokenSet?.access_token) {
throw new Error('Not authenticated');
}

return this.tokenSet.access_token;
}

get idToken(): string {
if (!this.isAuthenticated || !this.tokenSet?.id_token) {
throw new Error('Not authenticated');
}

return this.tokenSet.id_token;
}

get subject(): string {
if (!this.isAuthenticated || !this.tokenSet) {
throw new Error('Not authenticated');
}

return this.tokenSet.claims().sub;
}

public getClient(): Client {
if (!this.issuer) {
throw new Error('should init first');
Expand All @@ -56,6 +86,10 @@ export class LogtoClient {
return this.client;
}

public setToken(input: TokenSetParameters) {
this.tokenSet = new TokenSet(input);
}

public getLoginUrlAndCodeVerifier(redirectUri: string): [string, string] {
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
Expand All @@ -73,8 +107,9 @@ export class LogtoClient {

public async handleLoginCallback(redirectUri: string, codeVerifier: string, code: string) {
const client = this.getClient();
const tokenSet = await client.callback(redirectUri, { code }, { code_verifier: codeVerifier });
return tokenSet;
this.tokenSet = await client.callback(redirectUri, { code }, { code_verifier: codeVerifier });

return this.tokenSet;
}

private async initIssuer(url: string, onOidcReady?: () => void) {
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit 1a919bc

Please sign in to comment.