Skip to content

Commit

Permalink
feat(authentication): supportNative setting in providers (#1170)
Browse files Browse the repository at this point in the history
feat(authentication): common native auth init function for providers
refactor(authentication): change apple redirect init to use common function
feat(authentication): apple native login support
  • Loading branch information
kkopanidis authored Sep 26, 2024
1 parent e37d893 commit 028b315
Show file tree
Hide file tree
Showing 18 changed files with 192 additions and 32 deletions.
73 changes: 73 additions & 0 deletions modules/authentication/src/handlers/oauth2/OAuth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,42 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
return (this.initialized = true);
}

async initNative(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const scopes = call.request.params?.scopes ?? this.defaultScopes;
const { anonymousUser } = call.request.context;
const conduitUrl = (await this.grpcSdk.config.get('router')).hostUrl;

// returns part of regular redirect options for native usage
const queryOptions: Partial<RedirectOptions> = {
client_id: this.settings.clientId,
redirect_uri: conduitUrl + this.settings.callbackUrl,
scope: this.constructScopes(scopes),
};
const stateToken = await Token.getInstance()
.create({
tokenType: TokenType.STATE_TOKEN,
token: uuid(),
data: {
invitationToken: call.request.params?.invitationToken,
clientId: call.request.context.clientId,
scope: queryOptions.scope,
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
customRedirectUri: call.request.params.redirectUri,
anonymousUserId: anonymousUser?._id,
},
})
.catch(err => {
throw new GrpcError(status.INTERNAL, err);
});
queryOptions['state'] = stateToken.token;

return queryOptions;
}

nativeAuthorize(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
throw new GrpcError(status.INTERNAL, 'Not supported for current provider');
}

async redirect(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const scopes = call.request.params?.scopes ?? this.defaultScopes;
const { anonymousUser } = call.request.context;
Expand Down Expand Up @@ -334,6 +370,43 @@ export abstract class OAuth2<T, S extends OAuth2Settings>
),
this.redirect.bind(this),
);
if (this.settings.supportNative) {
routingManager.route(
{
path: `/initNative/${this.providerName}`,
description: `Begins ${this.capitalizeProvider()} native authentication.`,
action: ConduitRouteActions.GET,
queryParams: {
scopes: [ConduitString.Optional],
invitationToken: ConduitString.Optional,
captchaToken: ConduitString.Optional,
},
middlewares: initRouteMiddleware,
},
new ConduitRouteReturnDefinition(
`${this.capitalizeProvider()}InitNativeResponse`,
'String',
),
this.initNative.bind(this),
);
routingManager.route(
{
path: `/native/${this.providerName}`,
description: `Completes ${this.capitalizeProvider()} native authentication.`,
action: ConduitRouteActions.POST,
bodyParams: {
code: ConduitString.Required,
id_token: ConduitString.Required,
state: ConduitString.Required,
},
},
new ConduitRouteReturnDefinition(`${this.capitalizeProvider()}Response`, {
accessToken: ConduitString.Optional,
refreshToken: ConduitString.Optional,
}),
this.nativeAuthorize.bind(this),
);
}

if (this.settings.responseMode === 'query') {
routingManager.route(
Expand Down
3 changes: 2 additions & 1 deletion modules/authentication/src/handlers/oauth2/apple/apple.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"tokenUrl": "https://appleid.apple.com/auth/token",
"grantType": "authorization_code",
"responseType": "code id_token",
"responseMode": "form_post"
"responseMode": "form_post",
"supportNative": true
}
103 changes: 86 additions & 17 deletions modules/authentication/src/handlers/oauth2/apple/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,27 +161,96 @@ export class AppleHandlers extends OAuth2<AppleUser, AppleOAuth2Settings> {
);
}

declareRoutes(routingManager: RoutingManager) {
async nativeAuthorize(call: ParsedRouterRequest) {
const params = call.request.bodyParams;
const stateToken = await validateStateToken(params.state);
const decoded_id_token = jwt.decode(params.id_token, { complete: true });

const publicKeys = await axios.get('https://appleid.apple.com/auth/keys');
const publicKey = publicKeys.data.keys.find(
(key: Indexable) => key.kid === decoded_id_token!.header.kid,
);
const applePublicKey = await this.generateApplePublicKey(publicKey.kid);
this.verifyIdentityToken(applePublicKey, params.id_token);

const apple_private_key = this.settings.privateKey;

const jwtHeader = {
alg: 'ES256',
kid: this.settings.keyId,
};

const jwtPayload = {
iss: this.settings.teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400,
aud: 'https://appleid.apple.com',
sub: this.settings.clientId,
};

const apple_client_secret = jwt.sign(jwtPayload, apple_private_key, {
algorithm: 'ES256',
header: jwtHeader,
});

const clientId = this.settings.clientId;
const config = ConfigController.getInstance().config;
const initRouteMiddleware = ['authMiddleware?', 'checkAnonymousMiddleware'];
if (config.captcha.enabled && config.captcha.routes.oAuth2) {
initRouteMiddleware.unshift('captchaMiddleware');
}
routingManager.route(
{
path: `/init/apple`,
description: `Begins Apple authentication.`,
action: ConduitRouteActions.GET,
queryParams: {
invitationToken: ConduitString.Optional,
captchaConfig: ConduitString.Optional,
},
middlewares: initRouteMiddleware,
const postData = qs.stringify({
client_id: clientId,
client_secret: apple_client_secret,
code: params.code,
grant_type: this.settings.grantType,
});
const req = {
method: this.settings.accessTokenMethod,
url: this.settings.tokenUrl,
data: postData,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
new ConduitRouteReturnDefinition(`AppleInitResponse`, 'String'),
this.redirect.bind(this),
};
const appleResponseToken = await axios(req).catch(err => {
throw new GrpcError(status.INTERNAL, err.message);
});

const data = appleResponseToken.data;
const id_token = data.id_token;
const decoded = jwt.decode(id_token, { complete: true }) as Jwt;
const payload = decoded.payload as JwtPayload;
if (decoded_id_token!.payload.sub !== payload.sub) {
throw new GrpcError(status.INVALID_ARGUMENT, 'Invalid token');
}
let userData = params.user;
try {
userData = JSON.parse(params.user);
} catch (e) {
// already a valid object
}

const userParams = {
id: payload.sub!,
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);
ConduitGrpcSdk.Metrics?.increment('logged_in_users_total');

const conduitClientId = stateToken.data.clientId;

return TokenProvider.getInstance()!.provideUserTokens({
user,
clientId: conduitClientId,
config,
});
}

declareRoutes(routingManager: RoutingManager) {
super.declareRoutes(routingManager);
routingManager.route(
{
path: `/hook/apple`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"providerName": "bitbucket",
"tokenUrl": "https://bitbucket.org/site/oauth2/access_token",
"responseType": "code",
"grantType": "authorization_code"
}
"grantType": "authorization_code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"accessTokenMethod": "GET",
"authorizeUrl": "https://www.facebook.com/v11.0/dialog/oauth",
"tokenUrl": "https://graph.facebook.com/v12.0/oauth/access_token",
"responseType": "code"
"responseType": "code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { OAuth2 } from '../OAuth2.js';
import { FacebookUser } from './facebook.user.js';

// todo migrate to use native method properly
export class FacebookHandlers extends OAuth2<FacebookUser, OAuth2Settings> {
constructor(grpcSdk: ConduitGrpcSdk, config: { facebook: ProviderConfig }) {
super(grpcSdk, 'facebook', new OAuth2Settings(config.facebook, facebookParameters));
Expand Down
3 changes: 2 additions & 1 deletion modules/authentication/src/handlers/oauth2/figma/figma.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"authorizeUrl": "https://www.figma.com/oauth",
"tokenUrl": "https://www.figma.com/api/oauth/token",
"responseType": "code",
"grantType": "authorization_code"
"grantType": "authorization_code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"authorizeUrl": "https://github.com/login/oauth/authorize",
"providerName": "github",
"tokenUrl": "https://github.com/login/oauth/access_token",
"responseType": "code"
"responseType": "code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"responseType": "code",
"authorizeUrl": "https://gitlab.com/oauth/authorize",
"tokenUrl": "https://gitlab.com/oauth/token",
"grantType": "authorization_code"
"grantType": "authorization_code",
"supportNative": false
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"tokenUrl": "https://oauth2.googleapis.com/token",
"responseType": "code",
"grantType": "authorization_code",
"authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth"
"authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ProviderConfig,
} from '../interfaces/index.js';

// todo migrate to use native method properly
export class GoogleHandlers extends OAuth2<GoogleUser, OAuth2Settings> {
constructor(grpcSdk: ConduitGrpcSdk, config: { google: ProviderConfig }) {
super(grpcSdk, 'google', new OAuth2Settings(config.google, googleParameters));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class OAuth2Settings {
scopeSeperator?: string;
codeChallengeMethod?: string;
codeVerifier?: string;
supportNative: boolean;

constructor(
providerConfig: ProviderConfig,
Expand All @@ -31,6 +32,7 @@ export class OAuth2Settings {
responseType: string;
responseMode?: string;
codeChallengeMethod?: string;
supportNative?: boolean;
},
) {
this.accountLinking = providerConfig.accountLinking;
Expand All @@ -46,6 +48,7 @@ export class OAuth2Settings {
providerParams.responseMode === 'form_post' ? 'form_post' : 'query';
this.codeChallengeMethod = providerParams.codeChallengeMethod;
this.codeVerifier = !isNil(this.codeChallengeMethod) ? uuid() : undefined;
this.supportNative = providerParams.supportNative ?? false;
}

set provider(providerName: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"responseType": "code",
"authorizeUrl": "https://www.linkedin.com/oauth/v2/authorization",
"tokenUrl": "https://www.linkedin.com/oauth/v2/accessToken",
"grantType": "authorization_code"
}
"grantType": "authorization_code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"responseMode": "query",
"responseType": "code",
"grantType": "authorization_code"
"grantType": "authorization_code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"providerName": "reddit",
"tokenUrl": "https://www.reddit.com/api/v1/access_token",
"responseType": "code",
"grantType": "authorization_code"
"grantType": "authorization_code",
"supportNative": false
}
3 changes: 2 additions & 1 deletion modules/authentication/src/handlers/oauth2/slack/slack.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"authorizeUrl": "https://slack.com/oauth/authorize",
"providerName": "slack",
"tokenUrl": "https://slack.com/api/oauth.access",
"responseType": "code"
"responseType": "code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"providerName": "twitch",
"tokenUrl": "https://id.twitch.tv/oauth2/token",
"grantType": "authorization_code",
"responseType": "code"
"responseType": "code",
"supportNative": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"tokenUrl": "https://api.twitter.com/2/oauth2/token",
"responseType": "code",
"grantType": "authorization_code",
"codeChallengeMethod": "S256"
"codeChallengeMethod": "S256",
"supportNative": false
}

0 comments on commit 028b315

Please sign in to comment.