sources/oauth: implement apple native sign-in using the apple JS SDK
closes #1881 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -2,10 +2,15 @@ | ||||
| from time import time | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from django.http.request import HttpRequest | ||||
| from django.urls.base import reverse | ||||
| from jwt import decode, encode | ||||
| from rest_framework.fields import CharField | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes | ||||
| from authentik.sources.oauth.clients.oauth2 import OAuth2Client | ||||
| from authentik.sources.oauth.models import OAuthSource | ||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||
| from authentik.sources.oauth.views.callback import OAuthCallback | ||||
| from authentik.sources.oauth.views.redirect import OAuthRedirect | ||||
| @ -13,6 +18,22 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class AppleLoginChallenge(Challenge): | ||||
|     """Special challenge for apple-native authentication flow, which happens on the client.""" | ||||
|  | ||||
|     client_id = CharField() | ||||
|     component = CharField(default="ak-flow-sources-oauth-apple") | ||||
|     scope = CharField() | ||||
|     redirect_uri = CharField() | ||||
|     state = CharField() | ||||
|  | ||||
|  | ||||
| class AppleChallengeResponse(ChallengeResponse): | ||||
|     """Pseudo class for plex response""" | ||||
|  | ||||
|     component = CharField(default="ak-flow-sources-oauth-apple") | ||||
|  | ||||
|  | ||||
| class AppleOAuthClient(OAuth2Client): | ||||
|     """Apple OAuth2 client""" | ||||
|  | ||||
| @ -55,7 +76,7 @@ class AppleOAuthRedirect(OAuthRedirect): | ||||
|  | ||||
|     client_class = AppleOAuthClient | ||||
|  | ||||
|     def get_additional_parameters(self, source):  # pragma: no cover | ||||
|     def get_additional_parameters(self, source: OAuthSource):  # pragma: no cover | ||||
|         return { | ||||
|             "scope": "name email", | ||||
|             "response_mode": "form_post", | ||||
| @ -95,3 +116,24 @@ class AppleType(SourceType): | ||||
|  | ||||
|     def icon_url(self) -> str: | ||||
|         return "https://appleid.cdn-apple.com/appleid/button/logo" | ||||
|  | ||||
|     def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge: | ||||
|         """Pre-general all the things required for the JS SDK""" | ||||
|         apple_client = AppleOAuthClient( | ||||
|             source, | ||||
|             request, | ||||
|             callback=reverse( | ||||
|                 "authentik_sources_oauth:oauth-client-callback", | ||||
|                 kwargs={"source_slug": source.slug}, | ||||
|             ), | ||||
|         ) | ||||
|         args = apple_client.get_redirect_args() | ||||
|         return AppleLoginChallenge( | ||||
|             instance={ | ||||
|                 "client_id": apple_client.get_client_id(), | ||||
|                 "scope": "name email", | ||||
|                 "redirect_uri": args["redirect_uri"], | ||||
|                 "state": args["state"], | ||||
|                 "type": ChallengeTypes.NATIVE.value, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @ -25,6 +25,7 @@ from authentik.flows.challenge import ( | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView | ||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE | ||||
| from authentik.sources.oauth.types.apple import AppleLoginChallenge | ||||
| from authentik.sources.plex.models import PlexAuthenticationChallenge | ||||
| from authentik.stages.identification.models import IdentificationStage | ||||
| from authentik.stages.identification.signals import identification_failed | ||||
| @ -39,6 +40,7 @@ LOGGER = get_logger() | ||||
|         serializers={ | ||||
|             RedirectChallenge().fields["component"].default: RedirectChallenge, | ||||
|             PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge, | ||||
|             AppleLoginChallenge().fields["component"].default: AppleLoginChallenge, | ||||
|         }, | ||||
|         resource_type_field_name="component", | ||||
|     ) | ||||
|  | ||||
							
								
								
									
										46
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								schema.yml
									
									
									
									
									
								
							| @ -19086,6 +19086,46 @@ components: | ||||
|       - authentik.managed | ||||
|       - authentik.core | ||||
|       type: string | ||||
|     AppleChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Pseudo class for plex response | ||||
|       properties: | ||||
|         component: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|           default: ak-flow-sources-oauth-apple | ||||
|     AppleLoginChallenge: | ||||
|       type: object | ||||
|       description: Special challenge for apple-native authentication flow, which happens | ||||
|         on the client. | ||||
|       properties: | ||||
|         type: | ||||
|           $ref: '#/components/schemas/ChallengeChoices' | ||||
|         flow_info: | ||||
|           $ref: '#/components/schemas/ContextualFlowInfo' | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-flow-sources-oauth-apple | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
|             type: array | ||||
|             items: | ||||
|               $ref: '#/components/schemas/ErrorDetail' | ||||
|         client_id: | ||||
|           type: string | ||||
|         scope: | ||||
|           type: string | ||||
|         redirect_uri: | ||||
|           type: string | ||||
|         state: | ||||
|           type: string | ||||
|       required: | ||||
|       - client_id | ||||
|       - redirect_uri | ||||
|       - scope | ||||
|       - state | ||||
|       - type | ||||
|     Application: | ||||
|       type: object | ||||
|       description: Application Serializer | ||||
| @ -20225,6 +20265,7 @@ components: | ||||
|     ChallengeTypes: | ||||
|       oneOf: | ||||
|       - $ref: '#/components/schemas/AccessDeniedChallenge' | ||||
|       - $ref: '#/components/schemas/AppleLoginChallenge' | ||||
|       - $ref: '#/components/schemas/AuthenticatorDuoChallenge' | ||||
|       - $ref: '#/components/schemas/AuthenticatorSMSChallenge' | ||||
|       - $ref: '#/components/schemas/AuthenticatorStaticChallenge' | ||||
| @ -20246,6 +20287,7 @@ components: | ||||
|         propertyName: component | ||||
|         mapping: | ||||
|           ak-stage-access-denied: '#/components/schemas/AccessDeniedChallenge' | ||||
|           ak-flow-sources-oauth-apple: '#/components/schemas/AppleLoginChallenge' | ||||
|           ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallenge' | ||||
|           ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallenge' | ||||
|           ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallenge' | ||||
| @ -21387,6 +21429,7 @@ components: | ||||
|       - title | ||||
|     FlowChallengeResponseRequest: | ||||
|       oneOf: | ||||
|       - $ref: '#/components/schemas/AppleChallengeResponseRequest' | ||||
|       - $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest' | ||||
|       - $ref: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest' | ||||
|       - $ref: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest' | ||||
| @ -21405,6 +21448,7 @@ components: | ||||
|       discriminator: | ||||
|         propertyName: component | ||||
|         mapping: | ||||
|           ak-flow-sources-oauth-apple: '#/components/schemas/AppleChallengeResponseRequest' | ||||
|           ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest' | ||||
|           ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest' | ||||
|           ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest' | ||||
| @ -22711,11 +22755,13 @@ components: | ||||
|       oneOf: | ||||
|       - $ref: '#/components/schemas/RedirectChallenge' | ||||
|       - $ref: '#/components/schemas/PlexAuthenticationChallenge' | ||||
|       - $ref: '#/components/schemas/AppleLoginChallenge' | ||||
|       discriminator: | ||||
|         propertyName: component | ||||
|         mapping: | ||||
|           xak-flow-redirect: '#/components/schemas/RedirectChallenge' | ||||
|           ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallenge' | ||||
|           ak-flow-sources-oauth-apple: '#/components/schemas/AppleLoginChallenge' | ||||
|     LoginMetrics: | ||||
|       type: object | ||||
|       description: Login Metrics per 1h | ||||
|  | ||||
| @ -32,6 +32,7 @@ import "../elements/LoadingOverlay"; | ||||
| import { first } from "../utils"; | ||||
| import "./FlowInspector"; | ||||
| import "./access_denied/FlowAccessDenied"; | ||||
| import "./sources/apple/AppleLoginInit"; | ||||
| import "./sources/plex/PlexLoginInit"; | ||||
| import "./stages/RedirectStage"; | ||||
| import "./stages/authenticator_duo/AuthenticatorDuoStage"; | ||||
| @ -321,6 +322,11 @@ export class FlowExecutor extends LitElement implements StageHost { | ||||
|                             .host=${this as StageHost} | ||||
|                             .challenge=${this.challenge} | ||||
|                         ></ak-flow-sources-plex>`; | ||||
|                     case "ak-flow-sources-oauth-apple": | ||||
|                         return html`<ak-flow-sources-oauth-apple | ||||
|                             .host=${this as StageHost} | ||||
|                             .challenge=${this.challenge} | ||||
|                         ></ak-flow-sources-oauth-apple>`; | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
							
								
								
									
										79
									
								
								web/src/flows/sources/apple/AppleLoginInit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								web/src/flows/sources/apple/AppleLoginInit.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "../../../authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| import PFLogin from "@patternfly/patternfly/components/Login/login.css"; | ||||
| import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { AppleChallengeResponseRequest, AppleLoginChallenge } from "@goauthentik/api"; | ||||
|  | ||||
| import "../../../elements/EmptyState"; | ||||
| import { BaseStage } from "../../stages/base"; | ||||
|  | ||||
| @customElement("ak-flow-sources-oauth-apple") | ||||
| export class AppleLoginInit extends BaseStage<AppleLoginChallenge, AppleChallengeResponseRequest> { | ||||
|     @property({ type: Boolean }) | ||||
|     isModalShown = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         const appleAuth = document.createElement("script"); | ||||
|         appleAuth.src = | ||||
|             "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"; | ||||
|         appleAuth.type = "text/javascript"; | ||||
|         appleAuth.onload = () => { | ||||
|             AppleID.auth.init({ | ||||
|                 clientId: this.challenge?.clientId, | ||||
|                 scope: this.challenge.scope, | ||||
|                 redirectURI: this.challenge.redirectUri, | ||||
|                 state: this.challenge.state, | ||||
|                 usePopup: false, | ||||
|             }); | ||||
|             AppleID.auth.signIn(); | ||||
|             this.isModalShown = true; | ||||
|         }; | ||||
|         document.head.append(appleAuth); | ||||
|         //Listen for authorization success | ||||
|         document.addEventListener("AppleIDSignInOnSuccess", () => { | ||||
|             //handle successful response | ||||
|         }); | ||||
|         //Listen for authorization failures | ||||
|         document.addEventListener("AppleIDSignInOnFailure", (error) => { | ||||
|             console.warn(error); | ||||
|             this.isModalShown = false; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<header class="pf-c-login__main-header"> | ||||
|                 <h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Apple...`}</h1> | ||||
|             </header> | ||||
|             <div class="pf-c-login__main-body"> | ||||
|                 <form class="pf-c-form"> | ||||
|                     <ak-empty-state ?loading="${true}"> </ak-empty-state> | ||||
|                     ${!this.isModalShown | ||||
|                         ? html`<button | ||||
|                               class="pf-c-button pf-m-primary pf-m-block" | ||||
|                               @click=${() => { | ||||
|                                   AppleID.auth.signIn(); | ||||
|                               }} | ||||
|                           > | ||||
|                               ${t`Retry`} | ||||
|                           </button>` | ||||
|                         : html``} | ||||
|                 </form> | ||||
|             </div> | ||||
|             <footer class="pf-c-login__main-footer"> | ||||
|                 <ul class="pf-c-login__main-footer-links"></ul> | ||||
|             </footer>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										14
									
								
								web/src/flows/sources/apple/apple.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								web/src/flows/sources/apple/apple.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| declare namespace AppleID { | ||||
|     const auth: AppleIDAuth; | ||||
|  | ||||
|     class AppleIDAuth { | ||||
|         init({ | ||||
|             clientId: string, | ||||
|             scope: string, | ||||
|             redirectURI: string, | ||||
|             state: string, | ||||
|             usePopup: boolean, | ||||
|         }): void; | ||||
|         async signIn(): Promise<void>; | ||||
|     } | ||||
| } | ||||
| @ -437,6 +437,10 @@ msgstr "Audience" | ||||
| #~ msgid "Auth Type" | ||||
| #~ msgstr "Auth Type" | ||||
|  | ||||
| #: src/flows/sources/apple/AppleLoginInit.ts | ||||
| msgid "Authenticating with Apple..." | ||||
| msgstr "Authenticating with Apple..." | ||||
|  | ||||
| #: src/flows/sources/plex/PlexLoginInit.ts | ||||
| msgid "Authenticating with Plex..." | ||||
| msgstr "Authenticating with Plex..." | ||||
| @ -3784,6 +3788,10 @@ msgstr "Resources" | ||||
| msgid "Result" | ||||
| msgstr "Result" | ||||
|  | ||||
| #: src/flows/sources/apple/AppleLoginInit.ts | ||||
| msgid "Retry" | ||||
| msgstr "Retry" | ||||
|  | ||||
| #:  | ||||
| #~ msgid "Retry Task" | ||||
| #~ msgstr "Retry Task" | ||||
|  | ||||
| @ -441,6 +441,10 @@ msgstr "Audience" | ||||
| #~ msgid "Auth Type" | ||||
| #~ msgstr "" | ||||
|  | ||||
| #: src/flows/sources/apple/AppleLoginInit.ts | ||||
| msgid "Authenticating with Apple..." | ||||
| msgstr "" | ||||
|  | ||||
| #: src/flows/sources/plex/PlexLoginInit.ts | ||||
| msgid "Authenticating with Plex..." | ||||
| msgstr "Authentification avec Plex..." | ||||
| @ -3755,6 +3759,10 @@ msgstr "Ressources" | ||||
| msgid "Result" | ||||
| msgstr "Résultat" | ||||
|  | ||||
| #: src/flows/sources/apple/AppleLoginInit.ts | ||||
| msgid "Retry" | ||||
| msgstr "" | ||||
|  | ||||
| #~ msgid "Retry Task" | ||||
| #~ msgstr "Réessayer la tâche" | ||||
|  | ||||
|  | ||||
| @ -433,6 +433,10 @@ msgstr "" | ||||
| #~ msgid "Auth Type" | ||||
| #~ msgstr "" | ||||
|  | ||||
| #: src/flows/sources/apple/AppleLoginInit.ts | ||||
| msgid "Authenticating with Apple..." | ||||
| msgstr "" | ||||
|  | ||||
| #: src/flows/sources/plex/PlexLoginInit.ts | ||||
| msgid "Authenticating with Plex..." | ||||
| msgstr "" | ||||
| @ -3774,6 +3778,10 @@ msgstr "" | ||||
| msgid "Result" | ||||
| msgstr "" | ||||
|  | ||||
| #: src/flows/sources/apple/AppleLoginInit.ts | ||||
| msgid "Retry" | ||||
| msgstr "" | ||||
|  | ||||
| #:  | ||||
| #~ msgid "Retry Task" | ||||
| #~ msgstr "" | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer