From 830b8bcd5bfe7ef5eae354ac342cc74b53c3a8ad Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 6 Feb 2021 18:35:55 +0100 Subject: [PATCH] web: add page for OAuth2 Provider --- authentik/providers/oauth2/api.py | 62 ++++++- authentik/providers/oauth2/models.py | 37 ---- .../providers/oauth2/setup_url_modal.html | 50 ----- swagger.yaml | 42 +++++ web/src/api/providers/OAuth2.ts | 42 +++++ .../pages/providers/OAuth2ProviderViewPage.ts | 172 ++++++++++++++++++ web/src/pages/providers/ProviderViewPage.ts | 3 + web/src/utils.ts | 9 + 8 files changed, 329 insertions(+), 88 deletions(-) delete mode 100644 authentik/providers/oauth2/templates/providers/oauth2/setup_url_modal.html create mode 100644 web/src/api/providers/OAuth2.ts create mode 100644 web/src/pages/providers/OAuth2ProviderViewPage.ts diff --git a/authentik/providers/oauth2/api.py b/authentik/providers/oauth2/api.py index a9286229b..c58270764 100644 --- a/authentik/providers/oauth2/api.py +++ b/authentik/providers/oauth2/api.py @@ -1,9 +1,17 @@ """OAuth2Provider API Views""" -from rest_framework.serializers import ModelSerializer +from django.shortcuts import reverse +from drf_yasg2.utils import swagger_auto_schema +from rest_framework.decorators import action +from rest_framework.fields import ReadOnlyField +from rest_framework.generics import get_object_or_404 +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.viewsets import ModelViewSet from authentik.core.api.providers import ProviderSerializer from authentik.core.api.utils import MetaNameSerializer +from authentik.core.models import Provider from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping @@ -29,12 +37,64 @@ class OAuth2ProviderSerializer(ProviderSerializer): ] +class OAuth2ProviderSetupURLs(Serializer): + """OAuth2 Provider Metadata serializer""" + + issuer = ReadOnlyField() + authorize = ReadOnlyField() + token = ReadOnlyField() + user_info = ReadOnlyField() + provider_info = ReadOnlyField() + + def create(self, request: Request) -> Response: + raise NotImplementedError + + def update(self, request: Request) -> Response: + raise NotImplementedError + + class OAuth2ProviderViewSet(ModelViewSet): """OAuth2Provider Viewset""" queryset = OAuth2Provider.objects.all() serializer_class = OAuth2ProviderSerializer + @action(methods=["GET"], detail=True) + @swagger_auto_schema(responses={200: OAuth2ProviderSetupURLs(many=False)}) + # pylint: disable=invalid-name + def setup_urls(self, request: Request, pk: int) -> str: + """Return metadata as XML string""" + provider = get_object_or_404(OAuth2Provider, pk=pk) + data = { + "issuer": provider.get_issuer(request), + "authorize": request.build_absolute_uri( + reverse( + "authentik_providers_oauth2:authorize", + ) + ), + "token": request.build_absolute_uri( + reverse( + "authentik_providers_oauth2:token", + ) + ), + "user_info": request.build_absolute_uri( + reverse( + "authentik_providers_oauth2:userinfo", + ) + ), + "provider_info": None, + } + try: + data["provider_info"] = request.build_absolute_uri( + reverse( + "authentik_providers_oauth2:provider-info", + kwargs={"application_slug": provider.application.slug}, + ) + ) + except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member + pass + return Response(data) + class ScopeMappingSerializer(ModelSerializer, MetaNameSerializer): """ScopeMapping Serializer""" diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index 4ee7e4ef1..e1fb623bc 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -14,7 +14,6 @@ from django.conf import settings from django.db import models from django.forms import ModelForm from django.http import HttpRequest -from django.shortcuts import reverse from django.utils import dateformat, timezone from django.utils.translation import gettext_lazy as _ from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key @@ -25,7 +24,6 @@ from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User from authentik.crypto.models import CertificateKeyPair from authentik.events.models import Event, EventAction from authentik.events.utils import get_user -from authentik.lib.utils.template import render_to_string from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT @@ -309,41 +307,6 @@ class OAuth2Provider(Provider): jws = JWS(payload, alg=self.jwt_alg) return jws.sign_compact(keys) - def html_setup_urls(self, request: HttpRequest) -> Optional[str]: - """return template and context modal with URLs for authorize, token, openid-config, etc""" - try: - # pylint: disable=no-member - return render_to_string( - "providers/oauth2/setup_url_modal.html", - { - "provider": self, - "issuer": self.get_issuer(request), - "authorize": request.build_absolute_uri( - reverse( - "authentik_providers_oauth2:authorize", - ) - ), - "token": request.build_absolute_uri( - reverse( - "authentik_providers_oauth2:token", - ) - ), - "userinfo": request.build_absolute_uri( - reverse( - "authentik_providers_oauth2:userinfo", - ) - ), - "provider_info": request.build_absolute_uri( - reverse( - "authentik_providers_oauth2:provider-info", - kwargs={"application_slug": self.application.slug}, - ) - ), - }, - ) - except Provider.application.RelatedObjectDoesNotExist: - return None - class Meta: verbose_name = _("OAuth2/OpenID Provider") diff --git a/authentik/providers/oauth2/templates/providers/oauth2/setup_url_modal.html b/authentik/providers/oauth2/templates/providers/oauth2/setup_url_modal.html deleted file mode 100644 index f17b87810..000000000 --- a/authentik/providers/oauth2/templates/providers/oauth2/setup_url_modal.html +++ /dev/null @@ -1,50 +0,0 @@ -{% load i18n %} - - - -
-
-

{% trans 'Setup URLs' %}

-
- - -
-
diff --git a/swagger.yaml b/swagger.yaml index 2955a7f79..ac497df95 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -4305,6 +4305,24 @@ paths: description: A unique integer value identifying this OAuth2/OpenID Provider. required: true type: integer + /providers/oauth2/{id}/setup_urls/: + get: + operationId: providers_oauth2_setup_urls + description: Return metadata as XML string + parameters: [] + responses: + '200': + description: OAuth2 Provider Metadata serializer + schema: + $ref: '#/definitions/OAuth2ProviderSetupURLs' + tags: + - providers + parameters: + - name: id + in: path + description: A unique integer value identifying this OAuth2/OpenID Provider. + required: true + type: integer /providers/proxy/: get: operationId: providers_proxy_list @@ -8808,6 +8826,30 @@ definitions: enum: - global - per_provider + OAuth2ProviderSetupURLs: + description: OAuth2 Provider Metadata serializer + type: object + properties: + issuer: + title: Issuer + type: string + readOnly: true + authorize: + title: Authorize + type: string + readOnly: true + token: + title: Token + type: string + readOnly: true + user_info: + title: User info + type: string + readOnly: true + provider_info: + title: Provider info + type: string + readOnly: true ProxyProvider: description: ProxyProvider Serializer required: diff --git a/web/src/api/providers/OAuth2.ts b/web/src/api/providers/OAuth2.ts new file mode 100644 index 000000000..2e6e28d63 --- /dev/null +++ b/web/src/api/providers/OAuth2.ts @@ -0,0 +1,42 @@ +import { DefaultClient } from "../Client"; +import { Provider } from "../Providers"; + +export interface OAuth2SetupURLs { + + issuer?: string; + authorize: string; + token: string; + user_info: string; + provider_info?: string; + +} + +export class OAuth2Provider extends Provider { + client_type: string + client_id: string; + client_secret: string; + token_validity: string; + include_claims_in_id_token: boolean; + jwt_alg: string; + rsa_key: string; + redirect_uris: string; + sub_mode: string; + issuer_mode: string; + + constructor() { + super(); + throw Error(); + } + + static get(id: number): Promise { + return DefaultClient.fetch(["providers", "oauth2", id.toString()]); + } + + static getLaunchURls(id: number): Promise { + return DefaultClient.fetch(["providers", "oauth2", id.toString(), "setup_urls"]); + } + + static appUrl(rest: string): string { + return `/application/oauth2/${rest}`; + } +} diff --git a/web/src/pages/providers/OAuth2ProviderViewPage.ts b/web/src/pages/providers/OAuth2ProviderViewPage.ts new file mode 100644 index 000000000..532587890 --- /dev/null +++ b/web/src/pages/providers/OAuth2ProviderViewPage.ts @@ -0,0 +1,172 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { Provider } from "../../api/Providers"; +import { OAuth2Provider, OAuth2SetupURLs } from "../../api/providers/OAuth2"; +import { COMMON_STYLES } from "../../common/styles"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import "../../elements/CodeMirror"; +import "../../elements/Tabs"; +import { Page } from "../../elements/Page"; +import { convertToTitle } from "../../utils"; + +@customElement("ak-provider-oauth2-view") +export class OAuth2ProviderViewPage extends Page { + pageTitle(): string { + return gettext(`OAuth Provider ${this.provider?.name}`); + } + pageDescription(): string | undefined { + return; + } + pageIcon(): string { + return "pf-icon pf-icon-integration"; + } + + @property() + set args(value: { [key: string]: number }) { + this.providerID = value.id; + } + + @property({type: Number}) + set providerID(value: number) { + OAuth2Provider.get(value).then((app) => this.provider = app); + OAuth2Provider.getLaunchURls(value).then((urls) => this.providerUrls = urls); + } + + @property({ attribute: false }) + provider?: OAuth2Provider; + + @property({ attribute: false }) + providerUrls?: OAuth2SetupURLs; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + constructor() { + super(); + this.addEventListener("ak-refresh", () => { + if (!this.provider?.pk) return; + this.providerID = this.provider?.pk; + }); + } + + renderContent(): TemplateResult { + if (!this.provider) { + return html``; + } + return html` +
+
+
+
+
+
+
+
+ ${gettext("Name")} +
+
+
${this.provider.name}
+
+
+
+
+ ${gettext("Assigned to application")} +
+
+
+ ${this.provider.assigned_application_slug ? + html` + ${this.provider.assigned_application_name} + `: + html`-` + } +
+
+
+
+
+ ${gettext("Client type")} +
+
+
${convertToTitle(this.provider.client_type)}
+
+
+
+
+ ${gettext("Client ID")} +
+
+
${this.provider.client_id}
+
+
+
+
+ ${gettext("Redirect URIs")} +
+
+
${this.provider.redirect_uris}
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
`; + } +} diff --git a/web/src/pages/providers/ProviderViewPage.ts b/web/src/pages/providers/ProviderViewPage.ts index 7321929b9..3f522ab35 100644 --- a/web/src/pages/providers/ProviderViewPage.ts +++ b/web/src/pages/providers/ProviderViewPage.ts @@ -7,6 +7,7 @@ import "../../elements/buttons/SpinnerButton"; import { SpinnerSize } from "../../elements/Spinner"; import "./SAMLProviderViewPage"; +import "./OAuth2ProviderViewPage"; @customElement("ak-provider-view") export class ProviderViewPage extends LitElement { @@ -42,6 +43,8 @@ export class ProviderViewPage extends LitElement { switch (this.provider?.object_type) { case "saml": return html``; + case "oauth2": + return html``; default: return html`

Invalid provider type ${this.provider?.object_type}

`; } diff --git a/web/src/utils.ts b/web/src/utils.ts index f8639647e..2e5697717 100644 --- a/web/src/utils.ts +++ b/web/src/utils.ts @@ -24,6 +24,15 @@ export function convertToSlug(text: string): string { .replace(/[^\w-]+/g, ""); } +export function convertToTitle(text: string): string { + return text.replace( + /\w\S*/g, + function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + } + ); +} + export function truncate(input?: string, max = 10): string { input = input || ""; const array = input.trim().split(" ");