Skip to content

Commit

Permalink
Merge pull request #7 from evert/refresh_token
Browse files Browse the repository at this point in the history
refresh_token support.
  • Loading branch information
evert authored Mar 12, 2019
2 parents 41bfa7c + b34e218 commit c02af5b
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 8 deletions.
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export MYSQL_USER
export MYSQL_DATABASE
export MYSQL_PASSWORD

.PHONY:start run build test lint lint-fix start-dev watch inspect deploy
.PHONY:start run build test lint fix lint-fix start-dev watch inspect deploy
start: build
node dist/app.js

Expand All @@ -39,7 +39,6 @@ fix:
tslint -p . --fix

lint-fix: fix
tslint -p . --fix

start-dev:
ts-node src/app.js
Expand Down
3 changes: 2 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Changelog
* Default port is `8531`.
* Added a 'Getting started' guide.
* Added all database schemas to set up a new server.
* The `password` `grant_type` is now supported.
* The `password` grant type is now supported.
* Refreshing tokens now works.
* The `allowed_grant_types` is now actively enforced for every client.
* Returning correct OAuth2 error responses for more internal errors.

Expand Down
9 changes: 9 additions & 0 deletions mysql-schema/020-oauth2-client-id-int.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
SET NAMES utf8mb4;
START TRANSACTION;

INSERT INTO changelog VALUES (20, UNIX_TIMESTAMP());

ALTER TABLE oauth2_tokens
CHANGE oauth2_client_id oauth2_client_id INT UNSIGNED NOT NULL;

COMMIT;
37 changes: 33 additions & 4 deletions src/oauth2/controller/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ class TokenController extends BaseController {

async post(ctx: Context) {

const supportedGrantTypes = ['client_credentials', 'authorization_code', 'password'];

const supportedGrantTypes = ['client_credentials', 'authorization_code', 'refresh_token', 'password'];
const grantType = ctx.request.body.grant_type;

if (!supportedGrantTypes.includes(grantType)) {
Expand Down Expand Up @@ -44,12 +43,14 @@ class TokenController extends BaseController {
}

switch (grantType) {
case 'client_credentials' :
return this.clientCredentials(oauth2Client, ctx);
case 'authorization_code' :
return this.authorizationCode(oauth2Client, ctx);
case 'client_credentials' :
return this.clientCredentials(oauth2Client, ctx);
case 'password' :
return this.password(oauth2Client, ctx);
case 'refresh_token' :
return this.refreshToken(oauth2Client, ctx);
}

}
Expand Down Expand Up @@ -120,6 +121,34 @@ class TokenController extends BaseController {

}

async refreshToken(oauth2Client: OAuth2Client, ctx: Context) {

if (!ctx.request.body.refresh_token) {
throw new InvalidRequest('The "refresh_token" property is required');
}

const oldToken = await oauth2Service.getTokenByRefreshToken(
ctx.request.body.refresh_token
);

if (oldToken.clientId !== oauth2Client.id) {
throw new InvalidGrant('Refresh token was issued to a different client');
}

const token = await oauth2Service.generateTokenFromRefreshToken(
oauth2Client,
ctx.request.body.refresh_token
);

ctx.response.body = {
access_token: token.accessToken,
token_type: token.tokenType,
expires_in: token.accessTokenExpires - Math.round(Date.now() / 1000),
refresh_token: token.refreshToken,
};

}

/**
* We're overriding the default dipatcher to catch OAuth2 errors.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/oauth2/formats/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function metadata() {
token_endpoint: '/token',
scopes_supported: [],
response_types_supported: ['token'],
grant_types_supported: ['client_credentials', 'implicit', 'authorization_code'],
grant_types_supported: ['client_credentials', 'implicit', 'authorization_code', 'refresh_token'],
token_endpoint_auth_methods_supported: ['client_secret_basic'],
service_documentation: 'https://evertpot.com/',
};
Expand Down
72 changes: 72 additions & 0 deletions src/oauth2/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export async function generateTokenForUser(client: OAuth2Client, user: User): Pr
accessTokenExpires: accessTokenExpires,
tokenType: 'bearer',
userId: user.id,
clientId: client.id,
};

}
Expand Down Expand Up @@ -117,6 +118,7 @@ export async function generateTokenForClient(client: OAuth2Client): Promise<OAut
accessTokenExpires: accessTokenExpires,
tokenType: 'bearer',
userId: client.userId,
clientId: client.id,
};

}
Expand Down Expand Up @@ -156,6 +158,37 @@ export async function generateTokenFromCode(client: OAuth2Client, code: string):

}

/**
* This function is used for the 'refresh_token' grant.
*
* By specifying a refresh token, a new access/refresh token pair gets
* returned. This also expires the old token.
*/
export async function generateTokenFromRefreshToken(client: OAuth2Client, refreshToken: string): Promise<OAuth2Token> {

const oldToken = await getTokenByRefreshToken(refreshToken);
if (oldToken.clientId !== client.id) {
throw new UnauthorizedClient('The client_id associated with the refresh did not match with the authenticated client credentials');
}

await revokeToken(oldToken);
const user = await UserService.findById(oldToken.userId);
return generateTokenForUser(client, user);

}

/**
* Removes a token.
*
* This function will not throw an error if the token was deleted before.
*/
export async function revokeToken(token: OAuth2Token) {

const query = 'DELETE FROM oauth2_tokens WHERE access_token = ?';
await db.query(query, [token.accessToken]);

}

/**
* This function is used for the authorization_code grant flow.
*
Expand Down Expand Up @@ -233,6 +266,45 @@ export async function getTokenByAccessToken(accessToken: string): Promise<OAuth2
accessTokenExpires: row.access_token_expires,
tokenType: 'bearer',
userId: row.user_id,
clientId: row.oauth2_client_id,
};

}

/**
* Returns Token information for an existing Refresh Token.
*
* This function will throw NotFound if the token was not recognized.
*/
export async function getTokenByRefreshToken(refreshToken: string): Promise<OAuth2Token> {

const query = `
SELECT
oauth2_client_id,
access_token,
refresh_token,
user_id,
access_token_expires,
refresh_token_expires
FROM oauth2_tokens
WHERE
refresh_token = ? AND
refresh_token_expires > UNIX_TIMESTAMP()
`;

const result = await db.query(query, [refreshToken]);
if (!result[0].length) {
throw new NotFound('Refresh token not recognized');
}

const row: OAuth2TokenRecord = result[0][0];
return {
accessToken: row.access_token,
refreshToken: row.refresh_token,
accessTokenExpires: row.access_token_expires,
tokenType: 'bearer',
userId: row.user_id,
clientId: row.oauth2_client_id,
};

}
1 change: 1 addition & 0 deletions src/oauth2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type OAuth2Token = {
accessTokenExpires: number,
tokenType: 'bearer',
userId: number,
clientId: number,
};

export type OAuth2Code = {
Expand Down

0 comments on commit c02af5b

Please sign in to comment.