Skip to content

Commit

Permalink
Refresh token (#252)
Browse files Browse the repository at this point in the history
* add nextjs-auth0 example

* add refresh token test

* return refresh_token from /oauth/token

* tests passing

* rollback config

* add refresh_token functionality

* store only the user.id

* add changeset

* fix tests

* add refresh token test
  • Loading branch information
dagda1 authored Feb 11, 2023
1 parent 54a86db commit 7e4e918
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 43 deletions.
4 changes: 4 additions & 0 deletions .changes/refresh_token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
"@simulacrum/auth0-simulator": minor
---
Add the `refresh_token` flow
10 changes: 6 additions & 4 deletions packages/auth0/src/auth/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { JWKS, PRIVATE_KEY } from "./constants";

export const parseKey = (key: string): string => key.split("~~").join("\n");

export const createJsonWebToken = (
payload: Record<string, unknown>,
type SignPayload = Parameters<typeof sign>[0];

export function createJsonWebToken<P extends SignPayload>(
payload: P,
privateKey = parseKey(PRIVATE_KEY),
options: SignOptions = {
algorithm: "RS256",
keyid: JWKS.keys[0].kid,
}
): string => {
): string {
return sign(payload, privateKey, options);
};
}
21 changes: 21 additions & 0 deletions packages/auth0/src/auth/refresh-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { GrantType, RefreshToken } from '../types';
import { epochTime } from './date';
import { encode } from "base64-url";
import { assert } from 'assert-ts';

export function issueRefreshToken(scope: string, grantType: GrantType): boolean {
return grantType === 'refresh_token' || scope.includes('offline_access');
}

export function createRefreshToken({ exp, rotations = 0, scope, user, nonce }: Omit<RefreshToken, 'iat'>): string {
assert(!!user.id, `no identifier for user`);

return encode(JSON.stringify({
exp,
iat: epochTime(),
rotations,
scope,
user: { id: user.id },
nonce
}));
}
2 changes: 1 addition & 1 deletion packages/auth0/src/handlers/auth0-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Person } from '@simulacrum/server';
import type { Auth0Configuration, QueryParams, ResponseModes } from '../types';
import type { RequestHandler } from 'express';
import type { Auth0Configuration, QueryParams, ResponseModes } from '../types';
import { createLoginRedirectHandler } from './login-redirect';
import { createWebMessageHandler } from './web-message';
import { loginView } from '../views/login';
Expand Down
90 changes: 61 additions & 29 deletions packages/auth0/src/handlers/oauth-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert } from 'assert-ts';
import { decode as decodeBase64 } from 'base64-url';
import { decode, decode as decodeBase64 } from 'base64-url';
import { epochTime, expiresAt } from '../auth/date';
import { createJsonWebToken } from '../auth/jwt';
import { createRulesRunner } from '../rules/rules-runner';
Expand All @@ -13,7 +13,9 @@ import type {
AccessTokenPayload,
GrantType,
IdTokenData,
RefreshToken,
} from '../types';
import { createRefreshToken, issueRefreshToken } from '../auth/refresh-token';

export const createTokens = async ({
body,
Expand All @@ -36,43 +38,72 @@ export const createTokens = async ({
let scope = deriveScope({ scopeConfig, clientID, audience });

let accessToken = getBaseAccessToken({ iss, grant_type, scope, audience });
let user: Person | undefined;
let nonce: string | undefined;

if (grant_type === 'client_credentials') {
return { access_token: createJsonWebToken(accessToken) };
}
// TODO: check refresh_token expiry date
else if (grant_type === 'refresh_token') {
let { refresh_token: refreshTokenValue } = body;
let refreshToken: RefreshToken = JSON.parse(decode(refreshTokenValue));

let findUser = createPersonQuery(people);

user = findUser((person) => person.id === refreshToken.user.id);

nonce = refreshToken.nonce;
assert(!!nonce, `400::No nonce in request`);

} else {
let { user, nonce } = verifyUserExistsInStore({
let result = verifyUserExistsInStore({
people,
body,
grant_type,
});
let { idTokenData, userData } = getIdToken({
body,
iss,
user,
clientID,
nonce,
});

let context: RuleContext<Partial<AccessTokenPayload>, IdTokenData> = {
clientID,
accessToken: { scope, sub: idTokenData.sub },
idToken: idTokenData,
};

let rulesRunner = createRulesRunner(rulesDirectory);
// the rules mutate the values
await rulesRunner(userData, context);

return {
access_token: createJsonWebToken({
...accessToken,
...context.accessToken,
}),
id_token: createJsonWebToken({
...userData,
...context.idToken,
}),
};
user = result.user;
nonce = result.nonce;
}

assert(!!user, '500::No user found');

let { idTokenData, userData } = getIdToken({
body,
iss,
user,
clientID,
nonce,
});

let context: RuleContext<Partial<AccessTokenPayload>, IdTokenData> = {
clientID,
accessToken: { scope, sub: idTokenData.sub },
idToken: idTokenData,
};

let rulesRunner = createRulesRunner(rulesDirectory);
// the rules mutate the values
await rulesRunner(userData, context);

return {
access_token: createJsonWebToken({
...accessToken,
...context.accessToken,
}),
id_token: createJsonWebToken({
...userData,
...context.idToken,
}),
refresh_token: issueRefreshToken(scope, grant_type) ? createRefreshToken({
exp: idTokenData.exp,
rotations: 0,
scope,
user,
nonce
}) : undefined
};
};

export const getIdToken = ({
Expand All @@ -99,6 +130,7 @@ export const getIdToken = ({
};

assert(!!user.email, '500::User in store requires an email');

let idTokenData: IdTokenData = {
alg: 'RS256',
typ: 'JWT',
Expand Down
6 changes: 3 additions & 3 deletions packages/auth0/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ const createAuth0Service: ResourceServiceCreator = (slice, options) => ({
*init() {
let debug = !!slice.slice('debug').get();
let { port } = options;
let config = getConfig(slice.slice('options').slice('options').get());
let config = getConfig(slice.slice('options', 'options').get());

let serviceURL = () => getServiceUrl(slice.get());

let auth0Store = slice.slice('store').slice('auth0');
let auth0Store = slice.slice('store', 'auth0');
auth0Store.set({});

let store: Auth0Store = {
Expand All @@ -50,7 +50,7 @@ const createAuth0Service: ResourceServiceCreator = (slice, options) => ({

let people: Iterable<Person> = {
*[Symbol.iterator]() {
let values = Object.values(slice.slice('store').slice('people').get() ?? {});
let values = Object.values(slice.slice('store', 'people').get() ?? {});
for (let person of values) {
yield person as Person;
}
Expand Down
21 changes: 16 additions & 5 deletions packages/auth0/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type ReadonlyFields = 'audience' | 'clientID' | 'scope' | 'port';

// grant_type list as defined by auth0
// https://auth0.com/docs/get-started/applications/application-grant-types#spec-conforming-grants
export type GrantType = 'password' | 'client_credentials' | 'authorization_code';
export type GrantType = 'password' | 'client_credentials' | 'authorization_code' | 'refresh_token';

export type ScopeConfig =
| string
Expand Down Expand Up @@ -94,10 +94,21 @@ export interface AccessTokenPayload {
[key: string]: string | number | string[];
}

export interface IdToken {
payload: IdTokenData;
export interface RefreshToken {
iat: number;
exp: number;
rotations?: number;
scope: string;
sessionUid?: string;
user: { id: string };
nonce?: string;
}

export interface AccessToken {
payload: AccessTokenPayload;
type Token<P> = {
payload: P;
}

export type IdToken = Token<IdTokenData>;

export type AccessToken = Token<AccessTokenPayload>;

2 changes: 1 addition & 1 deletion packages/auth0/src/views/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const loginView = ({
href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
rel="stylesheet"
/>
<script src="https://cdn.auth0.com/js/auth0/9.16.0/auth0.min.js"></script>
<script src="https://cdn.auth0.com/js/auth0/9.19.0/auth0.js"></script>
</head>
<title>login</title>
<body>
Expand Down
Loading

0 comments on commit 7e4e918

Please sign in to comment.