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:
parent
e4841d54a1
commit
2993f506a7
|
@ -2,10 +2,15 @@
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from django.urls.base import reverse
|
||||||
from jwt import decode, encode
|
from jwt import decode, encode
|
||||||
|
from rest_framework.fields import CharField
|
||||||
from structlog.stdlib import get_logger
|
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.clients.oauth2 import OAuth2Client
|
||||||
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||||
|
@ -13,6 +18,22 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||||
LOGGER = get_logger()
|
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):
|
class AppleOAuthClient(OAuth2Client):
|
||||||
"""Apple OAuth2 client"""
|
"""Apple OAuth2 client"""
|
||||||
|
|
||||||
|
@ -55,7 +76,7 @@ class AppleOAuthRedirect(OAuthRedirect):
|
||||||
|
|
||||||
client_class = AppleOAuthClient
|
client_class = AppleOAuthClient
|
||||||
|
|
||||||
def get_additional_parameters(self, source): # pragma: no cover
|
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
|
||||||
return {
|
return {
|
||||||
"scope": "name email",
|
"scope": "name email",
|
||||||
"response_mode": "form_post",
|
"response_mode": "form_post",
|
||||||
|
@ -95,3 +116,24 @@ class AppleType(SourceType):
|
||||||
|
|
||||||
def icon_url(self) -> str:
|
def icon_url(self) -> str:
|
||||||
return "https://appleid.cdn-apple.com/appleid/button/logo"
|
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.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
|
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
|
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.sources.plex.models import PlexAuthenticationChallenge
|
||||||
from authentik.stages.identification.models import IdentificationStage
|
from authentik.stages.identification.models import IdentificationStage
|
||||||
from authentik.stages.identification.signals import identification_failed
|
from authentik.stages.identification.signals import identification_failed
|
||||||
|
@ -39,6 +40,7 @@ LOGGER = get_logger()
|
||||||
serializers={
|
serializers={
|
||||||
RedirectChallenge().fields["component"].default: RedirectChallenge,
|
RedirectChallenge().fields["component"].default: RedirectChallenge,
|
||||||
PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge,
|
PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge,
|
||||||
|
AppleLoginChallenge().fields["component"].default: AppleLoginChallenge,
|
||||||
},
|
},
|
||||||
resource_type_field_name="component",
|
resource_type_field_name="component",
|
||||||
)
|
)
|
||||||
|
|
46
schema.yml
46
schema.yml
|
@ -19086,6 +19086,46 @@ components:
|
||||||
- authentik.managed
|
- authentik.managed
|
||||||
- authentik.core
|
- authentik.core
|
||||||
type: string
|
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:
|
Application:
|
||||||
type: object
|
type: object
|
||||||
description: Application Serializer
|
description: Application Serializer
|
||||||
|
@ -20225,6 +20265,7 @@ components:
|
||||||
ChallengeTypes:
|
ChallengeTypes:
|
||||||
oneOf:
|
oneOf:
|
||||||
- $ref: '#/components/schemas/AccessDeniedChallenge'
|
- $ref: '#/components/schemas/AccessDeniedChallenge'
|
||||||
|
- $ref: '#/components/schemas/AppleLoginChallenge'
|
||||||
- $ref: '#/components/schemas/AuthenticatorDuoChallenge'
|
- $ref: '#/components/schemas/AuthenticatorDuoChallenge'
|
||||||
- $ref: '#/components/schemas/AuthenticatorSMSChallenge'
|
- $ref: '#/components/schemas/AuthenticatorSMSChallenge'
|
||||||
- $ref: '#/components/schemas/AuthenticatorStaticChallenge'
|
- $ref: '#/components/schemas/AuthenticatorStaticChallenge'
|
||||||
|
@ -20246,6 +20287,7 @@ components:
|
||||||
propertyName: component
|
propertyName: component
|
||||||
mapping:
|
mapping:
|
||||||
ak-stage-access-denied: '#/components/schemas/AccessDeniedChallenge'
|
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-duo: '#/components/schemas/AuthenticatorDuoChallenge'
|
||||||
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallenge'
|
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallenge'
|
||||||
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallenge'
|
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallenge'
|
||||||
|
@ -21387,6 +21429,7 @@ components:
|
||||||
- title
|
- title
|
||||||
FlowChallengeResponseRequest:
|
FlowChallengeResponseRequest:
|
||||||
oneOf:
|
oneOf:
|
||||||
|
- $ref: '#/components/schemas/AppleChallengeResponseRequest'
|
||||||
- $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
|
- $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
|
||||||
- $ref: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
|
- $ref: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
|
||||||
- $ref: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
|
- $ref: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
|
||||||
|
@ -21405,6 +21448,7 @@ components:
|
||||||
discriminator:
|
discriminator:
|
||||||
propertyName: component
|
propertyName: component
|
||||||
mapping:
|
mapping:
|
||||||
|
ak-flow-sources-oauth-apple: '#/components/schemas/AppleChallengeResponseRequest'
|
||||||
ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
|
ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
|
||||||
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
|
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
|
||||||
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
|
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
|
||||||
|
@ -22711,11 +22755,13 @@ components:
|
||||||
oneOf:
|
oneOf:
|
||||||
- $ref: '#/components/schemas/RedirectChallenge'
|
- $ref: '#/components/schemas/RedirectChallenge'
|
||||||
- $ref: '#/components/schemas/PlexAuthenticationChallenge'
|
- $ref: '#/components/schemas/PlexAuthenticationChallenge'
|
||||||
|
- $ref: '#/components/schemas/AppleLoginChallenge'
|
||||||
discriminator:
|
discriminator:
|
||||||
propertyName: component
|
propertyName: component
|
||||||
mapping:
|
mapping:
|
||||||
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
|
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
|
||||||
ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallenge'
|
ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallenge'
|
||||||
|
ak-flow-sources-oauth-apple: '#/components/schemas/AppleLoginChallenge'
|
||||||
LoginMetrics:
|
LoginMetrics:
|
||||||
type: object
|
type: object
|
||||||
description: Login Metrics per 1h
|
description: Login Metrics per 1h
|
||||||
|
|
|
@ -32,6 +32,7 @@ import "../elements/LoadingOverlay";
|
||||||
import { first } from "../utils";
|
import { first } from "../utils";
|
||||||
import "./FlowInspector";
|
import "./FlowInspector";
|
||||||
import "./access_denied/FlowAccessDenied";
|
import "./access_denied/FlowAccessDenied";
|
||||||
|
import "./sources/apple/AppleLoginInit";
|
||||||
import "./sources/plex/PlexLoginInit";
|
import "./sources/plex/PlexLoginInit";
|
||||||
import "./stages/RedirectStage";
|
import "./stages/RedirectStage";
|
||||||
import "./stages/authenticator_duo/AuthenticatorDuoStage";
|
import "./stages/authenticator_duo/AuthenticatorDuoStage";
|
||||||
|
@ -321,6 +322,11 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
.host=${this as StageHost}
|
.host=${this as StageHost}
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
></ak-flow-sources-plex>`;
|
></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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
#~ msgid "Auth Type"
|
||||||
#~ msgstr "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
|
#: src/flows/sources/plex/PlexLoginInit.ts
|
||||||
msgid "Authenticating with Plex..."
|
msgid "Authenticating with Plex..."
|
||||||
msgstr "Authenticating with Plex..."
|
msgstr "Authenticating with Plex..."
|
||||||
|
@ -3784,6 +3788,10 @@ msgstr "Resources"
|
||||||
msgid "Result"
|
msgid "Result"
|
||||||
msgstr "Result"
|
msgstr "Result"
|
||||||
|
|
||||||
|
#: src/flows/sources/apple/AppleLoginInit.ts
|
||||||
|
msgid "Retry"
|
||||||
|
msgstr "Retry"
|
||||||
|
|
||||||
#:
|
#:
|
||||||
#~ msgid "Retry Task"
|
#~ msgid "Retry Task"
|
||||||
#~ msgstr "Retry Task"
|
#~ msgstr "Retry Task"
|
||||||
|
|
|
@ -441,6 +441,10 @@ msgstr "Audience"
|
||||||
#~ msgid "Auth Type"
|
#~ msgid "Auth Type"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: src/flows/sources/apple/AppleLoginInit.ts
|
||||||
|
msgid "Authenticating with Apple..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/flows/sources/plex/PlexLoginInit.ts
|
#: src/flows/sources/plex/PlexLoginInit.ts
|
||||||
msgid "Authenticating with Plex..."
|
msgid "Authenticating with Plex..."
|
||||||
msgstr "Authentification avec Plex..."
|
msgstr "Authentification avec Plex..."
|
||||||
|
@ -3755,6 +3759,10 @@ msgstr "Ressources"
|
||||||
msgid "Result"
|
msgid "Result"
|
||||||
msgstr "Résultat"
|
msgstr "Résultat"
|
||||||
|
|
||||||
|
#: src/flows/sources/apple/AppleLoginInit.ts
|
||||||
|
msgid "Retry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#~ msgid "Retry Task"
|
#~ msgid "Retry Task"
|
||||||
#~ msgstr "Réessayer la tâche"
|
#~ msgstr "Réessayer la tâche"
|
||||||
|
|
||||||
|
|
|
@ -433,6 +433,10 @@ msgstr ""
|
||||||
#~ msgid "Auth Type"
|
#~ msgid "Auth Type"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: src/flows/sources/apple/AppleLoginInit.ts
|
||||||
|
msgid "Authenticating with Apple..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/flows/sources/plex/PlexLoginInit.ts
|
#: src/flows/sources/plex/PlexLoginInit.ts
|
||||||
msgid "Authenticating with Plex..."
|
msgid "Authenticating with Plex..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -3774,6 +3778,10 @@ msgstr ""
|
||||||
msgid "Result"
|
msgid "Result"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/flows/sources/apple/AppleLoginInit.ts
|
||||||
|
msgid "Retry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
#~ msgid "Retry Task"
|
#~ msgid "Retry Task"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
Reference in New Issue