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""" """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 rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.utils import MetaNameSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Provider
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping 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): class OAuth2ProviderViewSet(ModelViewSet):
"""OAuth2Provider Viewset""" """OAuth2Provider Viewset"""
queryset = OAuth2Provider.objects.all() queryset = OAuth2Provider.objects.all()
serializer_class = OAuth2ProviderSerializer 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): class ScopeMappingSerializer(ModelSerializer, MetaNameSerializer):
"""ScopeMapping Serializer""" """ScopeMapping Serializer"""

View File

@ -14,7 +14,6 @@ from django.conf import settings
from django.db import models from django.db import models
from django.forms import ModelForm from django.forms import ModelForm
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import reverse
from django.utils import dateformat, timezone from django.utils import dateformat, timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key 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.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.utils import get_user 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.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
@ -309,41 +307,6 @@ class OAuth2Provider(Provider):
jws = JWS(payload, alg=self.jwt_alg) jws = JWS(payload, alg=self.jwt_alg)
return jws.sign_compact(keys) 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: class Meta:
verbose_name = _("OAuth2/OpenID Provider") 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. description: A unique integer value identifying this OAuth2/OpenID Provider.
required: true required: true
type: integer 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/: /providers/proxy/:
get: get:
operationId: providers_proxy_list operationId: providers_proxy_list
@ -8808,6 +8826,30 @@ definitions:
enum: enum:
- global - global
- per_provider - 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: ProxyProvider:
description: ProxyProvider Serializer description: ProxyProvider Serializer
required: 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 { SpinnerSize } from "../../elements/Spinner";
import "./SAMLProviderViewPage"; import "./SAMLProviderViewPage";
import "./OAuth2ProviderViewPage";
@customElement("ak-provider-view") @customElement("ak-provider-view")
export class ProviderViewPage extends LitElement { export class ProviderViewPage extends LitElement {
@ -42,6 +43,8 @@ export class ProviderViewPage extends LitElement {
switch (this.provider?.object_type) { switch (this.provider?.object_type) {
case "saml": case "saml":
return html`<ak-provider-saml-view providerID=${this.provider.pk}></ak-provider-saml-view>`; 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: default:
return html`<p>Invalid provider type ${this.provider?.object_type}</p>`; 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, ""); .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 { export function truncate(input?: string, max = 10): string {
input = input || ""; input = input || "";
const array = input.trim().split(" "); const array = input.trim().split(" ");