Skip to content

Commit

Permalink
Merge pull request #486 from curveball/allow-tokens-to-not-expire
Browse files Browse the repository at this point in the history
Allow one-time-tokens to not expire during validation.
  • Loading branch information
evert authored Dec 1, 2023
2 parents cab0a5f + eb87c0f commit 4a079b7
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 18 deletions.
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog
-------------------

* Clients can now specify how long a one-time-token should be valid for.
* API clients can now request that one-time-tokens don't expire after use.
* The client_id is now validated to belong to the curent user when validating
one-time-tokens.


0.25.0 (2023-11-22)
Expand Down
27 changes: 27 additions & 0 deletions schemas/one-time-token-exchange.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://curveballjs.org/schemas/a12nserver/one-time-token-exchange.json",
"type": "object",
"description": "Request body of the exchange one-time-token endpoint.",
"required": ["token","client_id"],
"additionalProperties": false,

"properties": {
"token": {
"type": "string",
"description": "The token previously obtained with the 'generate one-time-token' endpoint."
},
"client_id": {
"type": "string",
"description": "The OAuth2 client_id. This client will be associated with the generated token."
},
"activateUser": {
"type": "boolean",
"description": "Activate the user if the token was valid."
},
"dontExpire": {
"type": "boolean",
"description": "Don't expire the one-time-token even if it was correct."
}
}
}
File renamed without changes.
23 changes: 12 additions & 11 deletions src/one-time-token/controller/exchange.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Controller from '@curveball/controller';
import { Context } from '@curveball/core';

import { Forbidden, UnprocessableEntity } from '@curveball/http-errors';
import { Forbidden } from '@curveball/http-errors';

import { tokenResponse } from '../../oauth2/formats/json';

Expand All @@ -14,24 +14,26 @@ type OtteRequest = {
activateUser?: boolean;
token: string;
client_id: string;
dontExpire?: boolean;
}

class OneTimeTokenExchangeController extends Controller {

async post(ctx: Context<OtteRequest>) {
async post(ctx: Context) {

ctx.privileges.require('a12n:one-time-token:exchange');
ctx.request.validate<OtteRequest>('https://curveballjs.org/schemas/a12nserver/one-time-token-exchange.json');

const principalService = new PrincipalService(ctx.privileges);

if (!ctx.request.body.token) {
throw new UnprocessableEntity('A token must be provided for the exchange');
}
if (!ctx.request.body.client_id) {
throw new UnprocessableEntity('A client_id must be provided for the exchange');
const client = await oauth2ClientService.findByClientId(ctx.request.body.client_id);
if (!ctx.privileges.isPrincipal(client.app)) {
throw new Forbidden(`The client_id ${ctx.request.body.client_id} is not associated with the currently authenticated app`);
}

const user = await tokenService.validateToken(ctx.request.body.token);

const user = await tokenService.validateToken(
ctx.request.body.token,
ctx.request.body.dontExpire ?? false,
);
if (!user.active) {
if (ctx.request.body.activateUser) {
user.active = true;
Expand All @@ -41,7 +43,6 @@ class OneTimeTokenExchangeController extends Controller {
}
}

const client = await oauth2ClientService.findByClientId(ctx.request.body.client_id);
const oauth2Token = await oauth2Service.generateTokenOneTimeToken({
client,
principal: user,
Expand Down
19 changes: 12 additions & 7 deletions src/one-time-token/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@ export async function createToken(user: User, expiresIn: number | null): Promise
* This function only works once for every token.
* After calling this function, the token automatically gets deleted.
*/
export async function validateToken(token: string): Promise<User> {
export async function validateToken(token: string, dontExpire: boolean = false): Promise<User> {

const query = 'SELECT token, user_id FROM reset_password_token WHERE token = ? AND expires_at > ?';
const result = await db.raw(query, [token, Math.floor(Date.now() / 1000)]);
const result = await db('reset_password_token')
.select()
.where({token})
.andWhere('expires_at', '>', Math.floor(Date.now() / 1000))
.first();

if (result[0].length !== 1) {
throw new BadRequest ('Failed to validate token');
if (!result) {
throw new BadRequest('Failed to validate token');
} else {
await db.raw('DELETE FROM reset_password_token WHERE token = ?', [token]);
if (!dontExpire) {
await db('reset_password_token').delete().where({token});
}
const principalService = new PrincipalService('insecure');
return principalService.findById(result[0][0].user_id) as Promise<User>;
return principalService.findById(result.user_id, 'user');
}

}

0 comments on commit 4a079b7

Please sign in to comment.