Skip to content

Commit

Permalink
feat(js): implement fetchTokenByRefreshToken
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao committed Jan 28, 2022
1 parent cbb213a commit 4e6600e
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 5 deletions.
3 changes: 2 additions & 1 deletion packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"report": "WITH_REPORT=true npm run package"
},
"dependencies": {
"@silverhand/essentials": "^1.1.4",
"@silverhand/essentials": "^1.1.5",
"jose": "^4.3.8",
"js-base64": "^3.7.2",
"lodash.get": "^4.4.2",
Expand All @@ -43,6 +43,7 @@
"text-encoder": "^0.0.4",
"ts-jest": "^27.0.4",
"ts-loader": "^9.2.6",
"type-fest": "^2.10.0",
"typescript": "^4.3.5",
"webpack": "^5.58.2",
"webpack-bundle-analyzer": "^4.5.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/js/src/consts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ContentType = {
formUrlEncoded: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
40 changes: 40 additions & 0 deletions packages/js/src/core/fetch-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { conditional, isNode } from '@silverhand/essentials';

import { fetchTokenByRefreshToken, RefreshTokenTokenResponse } from './fetch-token';

describe('fetch access token by providing valid refresh token', () => {
test('get token response', async () => {
const mockedOidcResponse = {
access_token: 'access_token',
refresh_token: 'refresh_token',
id_token: 'id_token',
scope: 'read register manage',
expires_in: 3600,
};

const expectedTokenResponse: RefreshTokenTokenResponse = {
accessToken: 'access_token',
refreshToken: 'refresh_token',
idToken: 'id_token',
scope: ['read', 'register', 'manage'],
expiresIn: 3600,
};

const fetchFunction = jest.fn().mockResolvedValue({
ok: true,
json: async () => mockedOidcResponse,
});

const tokenResponse = await fetchTokenByRefreshToken(
{
clientId: 'client_id',
tokenEndPoint: 'https://logto.dev/oidc/token',
refreshToken: 'refresh_token',
scope: ['read', 'register', 'manage'],
},
conditional(isNode() && fetchFunction)
);

expect(tokenResponse).toMatchObject(expectedTokenResponse);
});
});
64 changes: 64 additions & 0 deletions packages/js/src/core/fetch-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { KeysToCamelCase } from '@silverhand/essentials';
import { string, number, assert, type, optional, Infer } from 'superstruct';
import { Except } from 'type-fest';

import { ContentType } from '../consts';
import { createRequester } from '../utils/requester';

export interface FetchTokenByRefreshTokenParameters {
clientId: string;
tokenEndPoint: string;
refreshToken: string;
resource?: string;
scope?: string[];
}

const TokenResponseSchema = type({
access_token: string(),
refresh_token: string(),
id_token: string(),
scope: optional(string()),
expires_in: number(),
});

export type RefreshTokenTokenResponse = Except<
KeysToCamelCase<Infer<typeof TokenResponseSchema>>,
'scope'
> & {
scope?: string[];
};

export const fetchTokenByRefreshToken = async (
{ clientId, tokenEndPoint, refreshToken, resource, scope }: FetchTokenByRefreshTokenParameters,
fetchFunction?: typeof fetch
): Promise<RefreshTokenTokenResponse> => {
const parameters = new URLSearchParams();
parameters.append('client_id', clientId);
parameters.append('refresh_token', refreshToken);

if (resource) {
parameters.append('resource', resource);
}

if (scope?.length) {
parameters.append('scope', scope.join(' '));
}

const requester = createRequester(fetchFunction);
const response = await requester(tokenEndPoint, {
method: 'POST',
headers: ContentType.formUrlEncoded,
body: parameters,
});

assert(response, TokenResponseSchema);
const { access_token, refresh_token, id_token, scope: response_scope, expires_in } = response;

return {
accessToken: access_token,
refreshToken: refresh_token,
idToken: id_token,
scope: response_scope?.split(' '),
expiresIn: expires_in,
};
};
15 changes: 11 additions & 4 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 4e6600e

Please sign in to comment.