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...`}
+
+
+ `;
+ }
+}
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 ""