diff --git a/authentik/sources/oauth/types/apple.py b/authentik/sources/oauth/types/apple.py index 03bad20a9..6265b6ccd 100644 --- a/authentik/sources/oauth/types/apple.py +++ b/authentik/sources/oauth/types/apple.py @@ -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, + } + ) diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index e547408e3..f8c32674f 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -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", ) diff --git a/schema.yml b/schema.yml index 29522b730..39abf799e 100644 --- a/schema.yml +++ b/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 diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts index 47cf78973..1687eb485 100644 --- a/web/src/flows/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -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} >`; + case "ak-flow-sources-oauth-apple": + return html``; default: break; } diff --git a/web/src/flows/sources/apple/AppleLoginInit.ts b/web/src/flows/sources/apple/AppleLoginInit.ts new file mode 100644 index 000000000..3fa505caf --- /dev/null +++ b/web/src/flows/sources/apple/AppleLoginInit.ts @@ -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 { + @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`
+

${t`Authenticating with Apple...`}

+
+
+
+ + ${!this.isModalShown + ? html`` + : html``} +
+
+ `; + } +} diff --git a/web/src/flows/sources/apple/apple.d.ts b/web/src/flows/sources/apple/apple.d.ts new file mode 100644 index 000000000..c547d88d0 --- /dev/null +++ b/web/src/flows/sources/apple/apple.d.ts @@ -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; + } +} diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 7e849d917..f2b8fa1d6 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -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" diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index a1795f88b..e62b30b61 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -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" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 9ccb43584..1fd9a7f09 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -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 ""