web: add page for OAuth2 Provider

This commit is contained in:
Jens Langhammer 2021-02-06 18:35:55 +01:00
parent 0f5e6d0d8c
commit 830b8bcd5b
8 changed files with 329 additions and 88 deletions

View file

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

View file

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

View file

@ -1,50 +0,0 @@
{% load i18n %}
<ak-modal-button>
<button slot="trigger" class="pf-c-button pf-m-tertiary">
{% trans 'View Setup URLs' %}
</button>
<div slot="modal">
<div class="pf-c-modal-box__header">
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Setup URLs' %}</h1>
</div>
<div class="pf-c-modal-box__body" id="modal-description">
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">{% trans 'OpenID Configuration URL' %}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="{{ provider_info }}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">{% trans 'OpenID Configuration Issuer' %}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="{{ issuer }}" />
</div>
<hr>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">{% trans 'Authorize URL' %}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="{{ authorize }}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">{% trans 'Token URL' %}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="{{ token }}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">{% trans 'Userinfo Endpoint' %}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="{{ userinfo }}" />
</div>
</form>
</div>
<footer class="pf-c-modal-box__footer pf-m-align-left">
<a class="pf-c-button pf-m-primary">{% trans 'Close' %}</a>
</footer>
</div>
</ak-modal-button>

View file

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

View file

@ -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<OAuth2Provider> {
return DefaultClient.fetch<OAuth2Provider>(["providers", "oauth2", id.toString()]);
}
static getLaunchURls(id: number): Promise<OAuth2SetupURLs> {
return DefaultClient.fetch(["providers", "oauth2", id.toString(), "setup_urls"]);
}
static appUrl(rest: string): string {
return `/application/oauth2/${rest}`;
}
}

View file

@ -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`<ak-tabs>
<section slot="page-1" data-tab-title="${gettext("Overview")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<div class="pf-c-card pf-c-card-aggregate">
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Name")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.provider.name}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Assigned to application")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.assigned_application_slug ?
html`<a href="#/applications/${this.provider.assigned_application_slug}">
${this.provider.assigned_application_name}
</a>`:
html`-`
}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Client type")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${convertToTitle(this.provider.client_type)}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Client ID")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.provider.client_id}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Redirect URIs")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.provider.redirect_uris}</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-modal-button href="${Provider.adminUrl(`${this.provider.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</div>
</div>
</div>
</section>
<section slot="page-2" data-tab-title="${gettext("Metadata")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<div class="pf-c-card pf-c-card-aggregate">
<div class="pf-c-card__body">
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">${gettext("OpenID Configuration URL")}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="${this.providerUrls?.provider_info || "-"}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">${gettext("OpenID Configuration Issuer")}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="${this.providerUrls?.issuer || "-"}" />
</div>
<hr>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">${gettext("Authorize URL")}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="${this.providerUrls?.authorize || "-"}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">${gettext("Token URL")}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="${this.providerUrls?.token || "-"}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">${gettext("Userinfo Endpoint")}</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="${this.providerUrls?.user_info || "-"}" />
</div>
</form>
</div>
</div>
</div>
</div>
</section>
</ak-tabs>`;
}
}

View file

@ -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`<ak-provider-saml-view providerID=${this.provider.pk}></ak-provider-saml-view>`;
case "oauth2":
return html`<ak-provider-oauth2-view providerID=${this.provider.pk}></ak-provider-oauth2-view>`;
default:
return html`<p>Invalid provider type ${this.provider?.object_type}</p>`;
}

View file

@ -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(" ");