Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/graphql/v2/mutation/OAuthAuthorizationMutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const oAuthAuthorizationMutations = {
return {
id: userToken.id,
account: req.remoteUser.collective,
application: userToken.client,
application: userToken.application,
expiresAt: userToken.accessTokenExpiresAt,
createdAt: userToken.createdAt,
updatedAt: userToken.updatedAt,
Expand Down
2 changes: 1 addition & 1 deletion server/graphql/v2/object/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const GraphQLApplication = new GraphQLObjectType({
return {
id: userToken.id,
account: req.remoteUser.collective,
application: userToken.client,
application: userToken.application,
expiresAt: userToken.accessTokenExpiresAt,
createdAt: userToken.createdAt,
updatedAt: userToken.updatedAt,
Expand Down
2 changes: 1 addition & 1 deletion server/graphql/v2/object/Individual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const GraphQLIndividual = new GraphQLObjectType({
return {
id: row.id,
account: collective,
application: row.client,
application: row.application,
expiresAt: row.accessTokenExpiresAt,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down
8 changes: 7 additions & 1 deletion server/lib/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ class CustomTokenHandler extends TokenHandler {
auth.TOKEN_EXPIRATION_SESSION_OAUTH, // 90 days,
);

return new BearerTokenType(accessToken, auth.TOKEN_EXPIRATION_SESSION_OAUTH, null, model.scope.join(' '));
// Include refresh token in the response so clients can refresh access tokens
return new BearerTokenType(
accessToken,
auth.TOKEN_EXPIRATION_SESSION_OAUTH,
model.refreshToken || null,
model.scope.join(' '),
);
};
}

Expand Down
24 changes: 18 additions & 6 deletions server/lib/oauth/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ interface OauthModel extends AuthorizationCodeModel, RefreshTokenModel {}
export const dbApplicationToClient = (application: Application): OAuth2Server.Client => ({
id: application.clientId,
redirectUris: [application.callbackUrl],
grants: ['authorization_code'],
// Allow exchanging authorization codes and refreshing access tokens
grants: ['authorization_code', 'refresh_token'],
});

export const dbOAuthAuthorizationCodeToAuthorizationCode = (
Expand All @@ -49,6 +50,19 @@ export const dbOAuthAuthorizationCodeToAuthorizationCode = (
scope: authorization.scope,
});

const dbTokenToOAuthToken = async (token: any): Promise<Token> => {
if (!token.user && token.UserId) {
token.user = await models.User.findOne({ where: { id: token.UserId } });
}
if (!token.application && token.ApplicationId) {
token.application = await models.Application.findOne({ where: { id: token.ApplicationId } });
}
if (!token.client && token.application) {
token.client = dbApplicationToClient(token.application);
}
return token;
};

// For some reason `saveAuthorizationCode` and `saveToken` can receive a `scope`
// property that is a string[], string or undefined, and in the case of a string
// it may still be URL encoded. I wouldn't expected the framework to parse the
Expand Down Expand Up @@ -109,9 +123,7 @@ const model: OauthModel = {
scope,
preAuthorize2FA: Boolean(application.preAuthorize2FA),
});
oauthToken.user = user;
oauthToken.client = client;
return <Token>oauthToken;
return (await dbTokenToOAuthToken(oauthToken)) as Token;
} catch (e) {
debug(e);
// TODO: what should be thrown so it's properly catched on the library side?
Expand All @@ -138,7 +150,7 @@ const model: OauthModel = {
throw new InvalidTokenError('Invalid token');
}

return <Token>token;
return (await dbTokenToOAuthToken(token)) as Token;
},

async getRefreshToken(refreshToken): Promise<RefreshToken> {
Expand All @@ -148,7 +160,7 @@ const model: OauthModel = {
throw new InvalidTokenError('Invalid refresh token');
}

return <RefreshToken>token;
return (await dbTokenToOAuthToken(token)) as RefreshToken;
},

// -- Authorization code --
Expand Down
2 changes: 1 addition & 1 deletion server/models/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Application extends Model<InferAttributes<Application>, InferCreationAttri
declare public createdAt: CreationOptional<Date>;
declare public updatedAt: CreationOptional<Date>;
declare public deletedAt: CreationOptional<Date>;
declare public data: JSON;
declare public data: Record<string, unknown>;

declare public createdByUser: NonAttribute<User>;
declare getCreatedByUser: BelongsToGetAssociationMixin<User>;
Expand Down
5 changes: 4 additions & 1 deletion server/models/UserToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { CreationOptional, ForeignKey, InferAttributes, InferCreationAttrib
import oAuthScopes from '../constants/oauth-scopes';
import sequelize, { DataTypes, Model } from '../lib/sequelize';

import Application from './Application';
import User from './User';

export enum TokenType {
Expand All @@ -28,6 +29,8 @@ class UserToken extends Model<InferAttributes<UserToken>, InferCreationAttribute
declare public lastUsedAt: CreationOptional<Date>;

declare public user?: NonAttribute<User>;
declare public application?: NonAttribute<Application>;

declare public client?: NonAttribute<OAuth2Server.Client>;
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client property declaration should be removed since it's been replaced by the application property. Keeping both could lead to confusion and inconsistent usage.

Suggested change
declare public client?: NonAttribute<OAuth2Server.Client>;

Copilot uses AI. Check for mistakes.

hasScope(scope): boolean {
Expand Down Expand Up @@ -113,7 +116,7 @@ UserToken.init(
defaultScope: {
include: [
{ association: 'user', required: true },
{ association: 'client', required: true },
{ association: 'application', required: true },
],
},
},
Expand Down
2 changes: 1 addition & 1 deletion server/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ User.hasMany(UserToken, { foreignKey: 'UserId' });
User.hasMany(UserTwoFactorMethod);

// UserToken
UserToken.belongsTo(Application, { foreignKey: 'ApplicationId', as: 'client' });
UserToken.belongsTo(Application, { foreignKey: 'ApplicationId', as: 'application' });
UserToken.belongsTo(User, { foreignKey: 'UserId', as: 'user' });

// UserTwoFactorMethod
Expand Down
2 changes: 1 addition & 1 deletion server/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export default async (app: express.Application) => {
// 2) GraphQL v1 is not officially supported and should not be used by third party developers
if (req.userToken && req.userToken.type === 'OAUTH') {
// We need exceptions for prototype and internal tools
if (!req.userToken.client?.data?.enableGraphqlV1) {
if (!req.userToken.application?.data?.enableGraphqlV1) {
const errorMessage = 'OAuth access tokens are not accepted on GraphQL v1';
logger.warn(errorMessage);
return next(new errors.Unauthorized(errorMessage));
Expand Down
Loading