Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 modules/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"build:bundle": "rimraf bundle && node ../../libraries/service-bundle/dist/cli.js generate-manifest && tsup && node ../../libraries/service-bundle/dist/cli.js copy-assets && node ../../libraries/service-bundle/dist/cli.js generate-lockfile",
"build:docker": "docker build -t ghcr.io/conduitplatform/authentication:latest -f ./Dockerfile ../../ && docker push ghcr.io/conduitplatform/authentication:latest",
"generateTypes": "sh build.sh",
"test": "tsc -p tsconfig.test.json && copyfiles -u 1 src/data/*.json dist-test/ && node --test dist-test/utils/emailRestrictions.test.js dist-test/utils/jwtSecret.test.js dist-test/utils/jwtSecretReconciler.test.js dist-test/utils/biometricAuth.test.js dist-test/utils/appleIdentityToken.test.js dist-test/utils/appleSigningKey.test.js dist-test/handlers/oauth2/utils/resolveAppleOAuthClient.test.js dist-test/handlers/oauth2/utils/validateAppleClients.test.js dist-test/config/apple.config.test.js"
"test": "tsc -p tsconfig.test.json && copyfiles -u 1 src/data/*.json dist-test/ && node --test dist-test/utils/emailRestrictions.test.js dist-test/utils/jwtSecret.test.js dist-test/utils/jwtSecretReconciler.test.js dist-test/utils/biometricAuth.test.js dist-test/utils/appleIdentityToken.test.js dist-test/utils/appleSigningKey.test.js dist-test/handlers/oauth2/utils/resolveAppleOAuthClient.test.js dist-test/handlers/oauth2/utils/validateAppleClients.test.js dist-test/handlers/oauth2/utils/oauthMode.test.js dist-test/config/apple.config.test.js"
},
"license": "ISC",
"directories": {
Expand Down
6 changes: 6 additions & 0 deletions modules/authentication/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ export const errors = {
message: 'This email address is not allowed',
description: 'The provided email address is blocked by email restrictions',
},
REGISTRATION_NOT_ALLOWED: {
conduitCode: 'REGISTRATION_NOT_ALLOWED',
grpcCode: status.PERMISSION_DENIED,
message: 'User registration is not allowed for this request',
description: 'OAuth was started in sign-in-only mode and no existing user was found. Invitation tokens still allow registration.',
},
} as const satisfies Record<string, ModuleErrorDefinition>;
40 changes: 32 additions & 8 deletions modules/authentication/src/handlers/oauth2/OAuth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ import { TokenProvider } from '../tokenProvider.js';
import { v4 as uuid } from 'uuid';
import { createHash } from 'crypto';
import { TeamsHandler } from '../team.js';
import { validateStateToken } from './utils/index.js';
import {
assertOAuthRegistrationAllowed,
OAUTH_MODE_PARAM,
redirectOnRegistrationNotAllowed,
resolveOAuthMode,
type OAuthMode,
validateStateToken,
} from './utils/index.js';
import { IAuthenticationStrategy } from '../../interfaces/index.js';
import { TokenType } from '../../constants/index.js';
import {
Expand All @@ -39,6 +46,7 @@ import {
} from '../../constants/index.js';
import { AuthUtils } from '../../utils/index.js';
import { assertEmailAllowed } from '../../utils/emailRestrictions.js';
import { errors } from '../../errors.js';

export abstract class OAuth2<
T,
Expand Down Expand Up @@ -108,6 +116,7 @@ export abstract class OAuth2<
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
customRedirectUri: call.request.params.redirectUri,
anonymousUserId: anonymousUser?._id,
mode: resolveOAuthMode(call.request.params?.mode),
...this.getOAuthStateExtras(call),
},
})
Expand Down Expand Up @@ -162,6 +171,7 @@ export abstract class OAuth2<
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
customRedirectUri: call.request.params.redirectUri,
anonymousUserId: anonymousUser?._id,
mode: resolveOAuthMode(call.request.params?.mode),
...this.getOAuthStateExtras(call),
},
})
Expand Down Expand Up @@ -219,14 +229,20 @@ export abstract class OAuth2<
});

await Token.getInstance().deleteOne(stateToken);
const user = await this.createOrUpdateUser(
payload,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
);
const redirectUri =
AuthUtils.validateRedirectUri(stateToken.data.customRedirectUri) ??
this.settings.finalRedirect;
let user: User;
try {
user = await this.createOrUpdateUser(
payload,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
resolveOAuthMode(stateToken.data.mode),
);
} catch (err) {
return redirectOnRegistrationNotAllowed(err, redirectUri);
}

return TokenProvider.getInstance().provideUserTokens(
{
Expand All @@ -252,6 +268,7 @@ export abstract class OAuth2<
payload,
call.request.params.invitationToken,
call.request.context.anonymousUser?._id,
resolveOAuthMode(call.request.params?.mode),
);
const config = ConfigController.getInstance().config;
return TokenProvider.getInstance().provideUserTokens({
Expand All @@ -265,6 +282,7 @@ export abstract class OAuth2<
payload: Payload<T>,
invitationToken?: string,
anonymousUserId?: string,
mode: OAuthMode = 'both',
): Promise<User> {
let user: User | null = null;
if (payload.hasOwnProperty('email') && !isNil(payload.email)) {
Expand Down Expand Up @@ -315,6 +333,7 @@ export abstract class OAuth2<
if (!user.isVerified) user.isVerified = true;
user = await User.getInstance().findByIdAndUpdate(user._id, user);
} else {
assertOAuthRegistrationAllowed(mode, invitationToken);
if (payload.email) {
assertEmailAllowed(payload.email);
}
Expand Down Expand Up @@ -374,7 +393,7 @@ export abstract class OAuth2<
routingManager.route(
{
path: `/init/${this.providerName}`,
description: `Begins ${this.capitalizeProvider()} authentication.`,
description: `Begins ${this.capitalizeProvider()} authentication. Optional mode: "both" (default, login and register) or "signIn" (existing users only; invitation tokens still register).`,
action: ConduitRouteActions.GET,
queryParams: this.getInitRouteQueryParams(),
middlewares: initRouteMiddleware,
Expand All @@ -390,7 +409,7 @@ export abstract class OAuth2<
routingManager.route(
{
path: `/initNative/${this.providerName}`,
description: `Begins ${this.capitalizeProvider()} native authentication.`,
description: `Begins ${this.capitalizeProvider()} native authentication. Optional mode: "both" (default, login and register) or "signIn" (existing users only; invitation tokens still register).`,
action: ConduitRouteActions.GET,
queryParams: this.getInitNativeRouteQueryParams(),
middlewares: initRouteMiddleware,
Expand All @@ -413,6 +432,7 @@ export abstract class OAuth2<
state: ConduitString.Required,
user: ConduitJson.Optional,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
rateLimit: OAUTH_NATIVE_COMPLETE,
},
new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, {
Expand All @@ -433,6 +453,7 @@ export abstract class OAuth2<
code: ConduitString.Required,
state: ConduitString.Required,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
rateLimit: OAUTH_CALLBACK,
},
new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, {
Expand All @@ -451,6 +472,7 @@ export abstract class OAuth2<
code: ConduitString.Required,
state: ConduitString.Required,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
rateLimit: OAUTH_CALLBACK,
},
new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, {
Expand All @@ -468,6 +490,7 @@ export abstract class OAuth2<
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
redirectUri: ConduitString.Optional,
mode: OAUTH_MODE_PARAM,
};
}

Expand All @@ -476,6 +499,7 @@ export abstract class OAuth2<
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
mode: OAUTH_MODE_PARAM,
};
}

Expand Down
27 changes: 19 additions & 8 deletions modules/authentication/src/handlers/oauth2/apple/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import axios from 'axios';
import { AppleUser } from './apple.user.js';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { TokenProvider } from '../../tokenProvider.js';
import { Token } from '../../../models/index.js';
import { Token, User } from '../../../models/index.js';
import { status } from '@grpc/grpc-js';
import jwksRsa from 'jwks-rsa';
import {
validateStateToken,
resolveAppleOAuthClient,
validateAppleClients,
redirectOnRegistrationNotAllowed,
resolveOAuthMode,
} from '../utils/index.js';
import {
ConduitJson,
Expand All @@ -37,6 +39,7 @@ import { OAUTH_CALLBACK } from '../../../constants/index.js';
import { AuthUtils } from '../../../utils/index.js';
import { verifyAppleIdentityToken } from '../../../utils/appleIdentityToken.js';
import { resolveAppleSigningKey } from '../../../utils/appleSigningKey.js';
import { errors } from '../../../errors.js';

export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
private readonly jwksClient = jwksRsa({
Expand Down Expand Up @@ -158,16 +161,22 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
email: payload.email,
data: { ...userData, ...payload.email_verified },
};
const user = await this.createOrUpdateUser(
userParams,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
);
await Token.getInstance().deleteOne(stateToken);

const redirectUri =
AuthUtils.validateRedirectUri(stateToken.data.customRedirectUri) ??
providerClient.redirect_uri;
let user: User;
try {
user = await this.createOrUpdateUser(
userParams,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
resolveOAuthMode(stateToken.data.mode),
);
} catch (err) {
await Token.getInstance().deleteOne(stateToken);
return redirectOnRegistrationNotAllowed(err, redirectUri);
}
await Token.getInstance().deleteOne(stateToken);
const conduitClientId = stateToken.data.clientId;

return TokenProvider.getInstance()!.provideUserTokens(
Expand Down Expand Up @@ -234,6 +243,7 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
userParams,
stateToken.data.invitationToken,
stateToken.data.anonymousUserId,
resolveOAuthMode(stateToken.data.mode),
);
await Token.getInstance().deleteOne(stateToken);

Expand All @@ -259,6 +269,7 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
state: ConduitString.Required,
user: ConduitJson.Optional,
},
errors: [errors.REGISTRATION_NOT_ALLOWED],
rateLimit: OAUTH_CALLBACK,
},
new ConduitRouteReturnDefinition(`AppleResponse`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
} from '../interfaces/index.js';
import { OAuth2 } from '../OAuth2.js';
import { FacebookUser } from './facebook.user.js';
import { OAUTH_MODE_PARAM } from '../utils/index.js';
import { errors } from '../../../errors.js';

// todo migrate to use native method properly
export class FacebookHandlers extends OAuth2<FacebookUser, OAuth2Settings> {
Expand Down Expand Up @@ -80,14 +82,16 @@ export class FacebookHandlers extends OAuth2<FacebookUser, OAuth2Settings> {
{
path: '/facebook',
action: ConduitRouteActions.POST,
description: `Login/register with Facebook by providing a token from the client.`,
description: `Login/register with Facebook by providing a token from the client. Optional mode: "both" (default, login and register) or "signIn" (existing users only; invitation tokens still register).`,
bodyParams: {
access_token: ConduitString.Required,
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
scopes: [ConduitString.Optional],
mode: OAUTH_MODE_PARAM,
},
middlewares: ['authMiddleware?', 'checkAnonymousMiddleware'],
errors: [errors.REGISTRATION_NOT_ALLOWED],
rateLimit: OAUTH_NATIVE_COMPLETE,
},
new ConduitRouteReturnDefinition('FacebookResponse', {
Expand Down
6 changes: 5 additions & 1 deletion modules/authentication/src/handlers/oauth2/google/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
Payload,
ProviderConfig,
} from '../interfaces/index.js';
import { OAUTH_MODE_PARAM } from '../utils/index.js';
import { errors } from '../../../errors.js';

// todo migrate to use native method properly
export class GoogleHandlers extends OAuth2<GoogleUser, OAuth2Settings> {
Expand Down Expand Up @@ -62,16 +64,18 @@ export class GoogleHandlers extends OAuth2<GoogleUser, OAuth2Settings> {
{
path: '/google',
action: ConduitRouteActions.POST,
description: `Login/register with Google by providing a token from the client.`,
description: `Login/register with Google by providing a token from the client. Optional mode: "both" (default, login and register) or "signIn" (existing users only; invitation tokens still register).`,
bodyParams: {
id_token: ConduitString.Required,
access_token: ConduitString.Required,
expires_in: ConduitString.Optional,
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
scopes: [ConduitString.Optional],
mode: OAUTH_MODE_PARAM,
},
middlewares: ['authMiddleware?', 'checkAnonymousMiddleware'],
errors: [errors.REGISTRATION_NOT_ALLOWED],
rateLimit: OAUTH_NATIVE_COMPLETE,
},
new ConduitRouteReturnDefinition('GoogleResponse', {
Expand Down
1 change: 1 addition & 0 deletions modules/authentication/src/handlers/oauth2/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './ValidateStateToken.js';
export * from './MakeRequest.js';
export * from './resolveAppleOAuthClient.js';
export * from './validateAppleClients.js';
export * from './oauthMode.js';
Loading
Loading