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

View File

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

View File

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

View File

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

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" #~ 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"

View File

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

View File

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