diff --git a/modules/authentication/src/handlers/oauth2/OAuth2.ts b/modules/authentication/src/handlers/oauth2/OAuth2.ts index 235827f6b..087927a2d 100644 --- a/modules/authentication/src/handlers/oauth2/OAuth2.ts +++ b/modules/authentication/src/handlers/oauth2/OAuth2.ts @@ -68,6 +68,42 @@ export abstract class OAuth2 return (this.initialized = true); } + async initNative(call: ParsedRouterRequest): Promise { + 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 = { + 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 { + throw new GrpcError(status.INTERNAL, 'Not supported for current provider'); + } + async redirect(call: ParsedRouterRequest): Promise { const scopes = call.request.params?.scopes ?? this.defaultScopes; const { anonymousUser } = call.request.context; @@ -334,6 +370,43 @@ export abstract class OAuth2 ), 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( diff --git a/modules/authentication/src/handlers/oauth2/apple/apple.json b/modules/authentication/src/handlers/oauth2/apple/apple.json index 2933ca0a6..445a26210 100644 --- a/modules/authentication/src/handlers/oauth2/apple/apple.json +++ b/modules/authentication/src/handlers/oauth2/apple/apple.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/apple/apple.ts b/modules/authentication/src/handlers/oauth2/apple/apple.ts index 9188147e5..a98d5f096 100644 --- a/modules/authentication/src/handlers/oauth2/apple/apple.ts +++ b/modules/authentication/src/handlers/oauth2/apple/apple.ts @@ -161,27 +161,96 @@ export class AppleHandlers extends OAuth2 { ); } - 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`, diff --git a/modules/authentication/src/handlers/oauth2/bitbucket/bitbucket.json b/modules/authentication/src/handlers/oauth2/bitbucket/bitbucket.json index 2bf531eed..2aae2d780 100644 --- a/modules/authentication/src/handlers/oauth2/bitbucket/bitbucket.json +++ b/modules/authentication/src/handlers/oauth2/bitbucket/bitbucket.json @@ -4,5 +4,6 @@ "providerName": "bitbucket", "tokenUrl": "https://bitbucket.org/site/oauth2/access_token", "responseType": "code", - "grantType": "authorization_code" -} \ No newline at end of file + "grantType": "authorization_code", + "supportNative": false +} diff --git a/modules/authentication/src/handlers/oauth2/facebook/facebook.json b/modules/authentication/src/handlers/oauth2/facebook/facebook.json index 4fd5d8fb2..fdf51c923 100644 --- a/modules/authentication/src/handlers/oauth2/facebook/facebook.json +++ b/modules/authentication/src/handlers/oauth2/facebook/facebook.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/facebook/facebook.ts b/modules/authentication/src/handlers/oauth2/facebook/facebook.ts index d6a975d38..7d9b2ed2a 100644 --- a/modules/authentication/src/handlers/oauth2/facebook/facebook.ts +++ b/modules/authentication/src/handlers/oauth2/facebook/facebook.ts @@ -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 { constructor(grpcSdk: ConduitGrpcSdk, config: { facebook: ProviderConfig }) { super(grpcSdk, 'facebook', new OAuth2Settings(config.facebook, facebookParameters)); diff --git a/modules/authentication/src/handlers/oauth2/figma/figma.json b/modules/authentication/src/handlers/oauth2/figma/figma.json index 131b217db..8636c26d4 100644 --- a/modules/authentication/src/handlers/oauth2/figma/figma.json +++ b/modules/authentication/src/handlers/oauth2/figma/figma.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/github/github.json b/modules/authentication/src/handlers/oauth2/github/github.json index b0770b96f..c23669364 100644 --- a/modules/authentication/src/handlers/oauth2/github/github.json +++ b/modules/authentication/src/handlers/oauth2/github/github.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/gitlab/gitlab.json b/modules/authentication/src/handlers/oauth2/gitlab/gitlab.json index 2de139022..09f1992ec 100644 --- a/modules/authentication/src/handlers/oauth2/gitlab/gitlab.json +++ b/modules/authentication/src/handlers/oauth2/gitlab/gitlab.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/google/google.json b/modules/authentication/src/handlers/oauth2/google/google.json index 676d8d1a2..e84c9bd07 100644 --- a/modules/authentication/src/handlers/oauth2/google/google.json +++ b/modules/authentication/src/handlers/oauth2/google/google.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/google/google.ts b/modules/authentication/src/handlers/oauth2/google/google.ts index 61dbf85f7..4fb85d6e3 100644 --- a/modules/authentication/src/handlers/oauth2/google/google.ts +++ b/modules/authentication/src/handlers/oauth2/google/google.ts @@ -17,6 +17,7 @@ import { ProviderConfig, } from '../interfaces/index.js'; +// todo migrate to use native method properly export class GoogleHandlers extends OAuth2 { constructor(grpcSdk: ConduitGrpcSdk, config: { google: ProviderConfig }) { super(grpcSdk, 'google', new OAuth2Settings(config.google, googleParameters)); diff --git a/modules/authentication/src/handlers/oauth2/interfaces/OAuth2Settings.ts b/modules/authentication/src/handlers/oauth2/interfaces/OAuth2Settings.ts index 4d1526ab3..deefbb7bb 100644 --- a/modules/authentication/src/handlers/oauth2/interfaces/OAuth2Settings.ts +++ b/modules/authentication/src/handlers/oauth2/interfaces/OAuth2Settings.ts @@ -20,6 +20,7 @@ export class OAuth2Settings { scopeSeperator?: string; codeChallengeMethod?: string; codeVerifier?: string; + supportNative: boolean; constructor( providerConfig: ProviderConfig, @@ -31,6 +32,7 @@ export class OAuth2Settings { responseType: string; responseMode?: string; codeChallengeMethod?: string; + supportNative?: boolean; }, ) { this.accountLinking = providerConfig.accountLinking; @@ -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) { diff --git a/modules/authentication/src/handlers/oauth2/linkedIn/linkedin.json b/modules/authentication/src/handlers/oauth2/linkedIn/linkedin.json index c4fe2bb2b..b2e78680d 100644 --- a/modules/authentication/src/handlers/oauth2/linkedIn/linkedin.json +++ b/modules/authentication/src/handlers/oauth2/linkedIn/linkedin.json @@ -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" -} \ No newline at end of file + "grantType": "authorization_code", + "supportNative": false +} diff --git a/modules/authentication/src/handlers/oauth2/microsoft/microsoft.json b/modules/authentication/src/handlers/oauth2/microsoft/microsoft.json index 2d1629c09..051303b3d 100644 --- a/modules/authentication/src/handlers/oauth2/microsoft/microsoft.json +++ b/modules/authentication/src/handlers/oauth2/microsoft/microsoft.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/reddit/reddit.json b/modules/authentication/src/handlers/oauth2/reddit/reddit.json index 6ef0a5e01..5d6a15dea 100644 --- a/modules/authentication/src/handlers/oauth2/reddit/reddit.json +++ b/modules/authentication/src/handlers/oauth2/reddit/reddit.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/slack/slack.json b/modules/authentication/src/handlers/oauth2/slack/slack.json index f5243acbc..cd173d592 100644 --- a/modules/authentication/src/handlers/oauth2/slack/slack.json +++ b/modules/authentication/src/handlers/oauth2/slack/slack.json @@ -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 } diff --git a/modules/authentication/src/handlers/oauth2/twitch/twitch.json b/modules/authentication/src/handlers/oauth2/twitch/twitch.json index 064d67628..c6a57d25a 100644 --- a/modules/authentication/src/handlers/oauth2/twitch/twitch.json +++ b/modules/authentication/src/handlers/oauth2/twitch/twitch.json @@ -4,5 +4,6 @@ "providerName": "twitch", "tokenUrl": "https://id.twitch.tv/oauth2/token", "grantType": "authorization_code", - "responseType": "code" + "responseType": "code", + "supportNative": false } diff --git a/modules/authentication/src/handlers/oauth2/twitter/twitter.json b/modules/authentication/src/handlers/oauth2/twitter/twitter.json index ab32918c1..c6475eddb 100644 --- a/modules/authentication/src/handlers/oauth2/twitter/twitter.json +++ b/modules/authentication/src/handlers/oauth2/twitter/twitter.json @@ -5,5 +5,6 @@ "tokenUrl": "https://api.twitter.com/2/oauth2/token", "responseType": "code", "grantType": "authorization_code", - "codeChallengeMethod": "S256" + "codeChallengeMethod": "S256", + "supportNative": false }