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:
Jens Langhammer 2021-12-14 00:40:29 +01:00
parent e4841d54a1
commit 2993f506a7
9 changed files with 214 additions and 1 deletions

View file

@ -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,
}
)

View file

@ -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",
)

View file

@ -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

View file

@ -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;
}

View 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
View 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>;
}
}

View file

@ -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"

View file

@ -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"

View file

@ -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 ""