Add self-service client registration
parent
dcc698cbec
commit
54e1ba0608
|
@ -0,0 +1,28 @@
|
||||||
|
"""Add Client.owner_id
|
||||||
|
|
||||||
|
Revision ID: dd58bc95a904
|
||||||
|
Revises: 5d43eb9bfe78
|
||||||
|
Create Date: 2020-05-25 17:55:05.506518
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'dd58bc95a904'
|
||||||
|
down_revision = '5d43eb9bfe78'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('oauth2_client', sa.Column('owner_id', sa.String(length=40), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('oauth2_client', 'owner_id')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -1,5 +1,6 @@
|
||||||
alembic==1.4.2
|
alembic==1.4.2
|
||||||
Authlib==0.14.3
|
Authlib==0.14.3
|
||||||
|
cached-property==1.5.1
|
||||||
certifi==2020.4.5.1
|
certifi==2020.4.5.1
|
||||||
cffi==1.14.0
|
cffi==1.14.0
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
|
|
|
@ -21,14 +21,14 @@ def create_app():
|
||||||
app.register_blueprint(sso.views.bp)
|
app.register_blueprint(sso.views.bp)
|
||||||
|
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
if app.config.get('PROXYFIX_ENABLE'):
|
|
||||||
print('gnuj')
|
if app.config.get("PROXYFIX_ENABLE"):
|
||||||
app.wsgi_app = ProxyFix(
|
app.wsgi_app = ProxyFix(
|
||||||
app.wsgi_app,
|
app.wsgi_app,
|
||||||
app.config.get('PROXYFIX_NUM_PROXIES'),
|
app.config.get("PROXYFIX_NUM_PROXIES"),
|
||||||
app.config.get('PROXYFIX_NUM_PROXIES'),
|
app.config.get("PROXYFIX_NUM_PROXIES"),
|
||||||
app.config.get('PROXYFIX_NUM_PROXIES'),
|
app.config.get("PROXYFIX_NUM_PROXIES"),
|
||||||
app.config.get('PROXYFIX_NUM_PROXIES'),
|
app.config.get("PROXYFIX_NUM_PROXIES"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -2,8 +2,8 @@ import re
|
||||||
import requests
|
import requests
|
||||||
import ldap
|
import ldap
|
||||||
import logging
|
import logging
|
||||||
|
from cached_property import cached_property
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
|
||||||
from sso.extensions import login_manager
|
from sso.extensions import login_manager
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,7 +47,6 @@ class LDAPUserProxy(object):
|
||||||
if len(res) != 1:
|
if len(res) != 1:
|
||||||
raise Exception("No such username.")
|
raise Exception("No such username.")
|
||||||
dn, data = res[0]
|
dn, data = res[0]
|
||||||
print(dn, data)
|
|
||||||
|
|
||||||
self.username = data.get("uid", [b""])[0].decode() or None
|
self.username = data.get("uid", [b""])[0].decode() or None
|
||||||
self.gecos = data.get("gecos", [b""])[0].decode() or None
|
self.gecos = data.get("gecos", [b""])[0].decode() or None
|
||||||
|
@ -63,7 +62,7 @@ class LDAPUserProxy(object):
|
||||||
def email(self):
|
def email(self):
|
||||||
return self.username + "@hackerspace.pl"
|
return self.username + "@hackerspace.pl"
|
||||||
|
|
||||||
# @cached_property
|
@cached_property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
url = "https://kasownik.hackerspace.pl/api/judgement/{}.json"
|
url = "https://kasownik.hackerspace.pl/api/judgement/{}.json"
|
||||||
try:
|
try:
|
||||||
|
|
50
sso/forms.py
50
sso/forms.py
|
@ -1,9 +1,55 @@
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, BooleanField
|
from wtforms import (
|
||||||
from wtforms.validators import DataRequired
|
StringField,
|
||||||
|
PasswordField,
|
||||||
|
BooleanField,
|
||||||
|
SelectField,
|
||||||
|
SelectMultipleField,
|
||||||
|
FieldList,
|
||||||
|
widgets,
|
||||||
|
)
|
||||||
|
from wtforms.validators import DataRequired, URL
|
||||||
|
|
||||||
|
|
||||||
|
class MultiCheckboxField(SelectMultipleField):
|
||||||
|
"""
|
||||||
|
A multiple-select, except displays a list of checkboxes.
|
||||||
|
|
||||||
|
Iterating the field will produce subfields, allowing custom rendering of
|
||||||
|
the enclosed checkbox fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
widget = widgets.ListWidget(prefix_label=False)
|
||||||
|
option_widget = widgets.CheckboxInput()
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
username = StringField("username", validators=[DataRequired()])
|
username = StringField("username", validators=[DataRequired()])
|
||||||
password = PasswordField("password", validators=[DataRequired()])
|
password = PasswordField("password", validators=[DataRequired()])
|
||||||
remember = BooleanField("remember me")
|
remember = BooleanField("remember me")
|
||||||
|
|
||||||
|
|
||||||
|
class ClientForm(FlaskForm):
|
||||||
|
client_name = StringField("Client name", validators=[DataRequired()])
|
||||||
|
client_uri = StringField("Client URI", validators=[DataRequired(), URL()])
|
||||||
|
redirect_uris = FieldList(
|
||||||
|
StringField("Redirect URI", validators=[DataRequired(), URL()]), min_entries=1
|
||||||
|
)
|
||||||
|
grant_types = MultiCheckboxField(
|
||||||
|
"Grant types",
|
||||||
|
choices=[("authorization_code", "authorization_code")],
|
||||||
|
validators=[DataRequired()],
|
||||||
|
default=["authorization_code"],
|
||||||
|
)
|
||||||
|
response_types = MultiCheckboxField(
|
||||||
|
"Response types",
|
||||||
|
choices=[("code", "code")],
|
||||||
|
validators=[DataRequired()],
|
||||||
|
default=["code"],
|
||||||
|
)
|
||||||
|
|
||||||
|
token_endpoint_auth_method = SelectField(
|
||||||
|
"Token endpoint authentication method",
|
||||||
|
choices=[("client_secret_basic", "Basic"), ("client_secret_post", "POST")],
|
||||||
|
validators=[DataRequired()],
|
||||||
|
)
|
||||||
|
|
|
@ -12,6 +12,8 @@ class Client(db.Model, OAuth2ClientMixin):
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
owner_id = db.Column(db.String(40), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin):
|
class AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin):
|
||||||
__tablename__ = "oauth2_code"
|
__tablename__ = "oauth2_code"
|
||||||
|
|
|
@ -113,10 +113,32 @@ class HybridGrant(_OpenIDHybridGrant):
|
||||||
authorization = AuthorizationServer()
|
authorization = AuthorizationServer()
|
||||||
require_oauth = ResourceProtector()
|
require_oauth = ResourceProtector()
|
||||||
|
|
||||||
|
def save_token(token, request):
|
||||||
|
if request.user:
|
||||||
|
user_id = request.user.get_user_id()
|
||||||
|
else:
|
||||||
|
user_id = None
|
||||||
|
client = request.client
|
||||||
|
|
||||||
|
# FIXME: is this the correct way of handling this? left for backward
|
||||||
|
# compatiblity
|
||||||
|
toks = Token.query.filter_by(client_id=client.client_id,
|
||||||
|
user_id=user_id)
|
||||||
|
|
||||||
|
# make sure that every client has only one token connected to a user
|
||||||
|
for t in toks:
|
||||||
|
db.session.delete(t)
|
||||||
|
|
||||||
|
item = Token(
|
||||||
|
client_id=client.client_id,
|
||||||
|
user_id=user_id,
|
||||||
|
**token
|
||||||
|
)
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def config_oauth(app):
|
def config_oauth(app):
|
||||||
query_client = create_query_client_func(db.session, Client)
|
query_client = create_query_client_func(db.session, Client)
|
||||||
save_token = create_save_token_func(db.session, Token)
|
|
||||||
authorization.init_app(app, query_client=query_client, save_token=save_token)
|
authorization.init_app(app, query_client=query_client, save_token=save_token)
|
||||||
|
|
||||||
# support all openid grants
|
# support all openid grants
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
from sqlalchemy.orm import exc
|
||||||
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_or_404(model, *criterion):
|
||||||
|
try:
|
||||||
|
rv = model.query.filter(*criterion).one()
|
||||||
|
except (exc.NoResultFound, exc.MultipleResultsFound):
|
||||||
|
abort(404)
|
||||||
|
else:
|
||||||
|
return rv
|
60
sso/views.py
60
sso/views.py
|
@ -9,12 +9,15 @@ from flask import (
|
||||||
jsonify,
|
jsonify,
|
||||||
current_app,
|
current_app,
|
||||||
)
|
)
|
||||||
|
import uuid
|
||||||
from flask_login import login_required, current_user, login_user, logout_user
|
from flask_login import login_required, current_user, login_user, logout_user
|
||||||
from sso.directory import LDAPUserProxy, check_credentials
|
from sso.directory import LDAPUserProxy, check_credentials
|
||||||
from sso.models import db, Token
|
from sso.models import db, Token, Client
|
||||||
from sso.forms import LoginForm
|
from sso.forms import LoginForm, ClientForm
|
||||||
|
from sso.utils import get_object_or_404
|
||||||
from sso.oauth2 import authorization, require_oauth
|
from sso.oauth2 import authorization, require_oauth
|
||||||
from authlib.oauth2 import OAuth2Error
|
from authlib.oauth2 import OAuth2Error
|
||||||
|
from authlib.common.security import generate_token
|
||||||
from authlib.integrations.flask_oauth2 import current_token
|
from authlib.integrations.flask_oauth2 import current_token
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,7 +30,8 @@ bp = Blueprint("sso", __name__)
|
||||||
def profile():
|
def profile():
|
||||||
return render_template(
|
return render_template(
|
||||||
"profile.html",
|
"profile.html",
|
||||||
tokens=Token.query.filter(Token.user_id == current_user.username),
|
tokens=Token.query.filter(Token.user_id == current_user.get_user_id()),
|
||||||
|
clients=Client.query.filter(Client.owner_id == current_user.get_user_id()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,6 +72,45 @@ def logout():
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/client/create", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def client_create():
|
||||||
|
form = ClientForm()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
client = Client()
|
||||||
|
client.client_id = uuid.uuid4()
|
||||||
|
client.client_secret = generate_token()
|
||||||
|
client.owner_id = current_user.get_user_id()
|
||||||
|
client.set_client_metadata(form.data)
|
||||||
|
|
||||||
|
db.session.add(client)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for(".client_edit", client_id=client.id))
|
||||||
|
|
||||||
|
return render_template("client_edit.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/client/<client_id>", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def client_edit(client_id):
|
||||||
|
client = get_object_or_404(
|
||||||
|
Client, Client.id == client_id, Client.owner_id == current_user.get_user_id()
|
||||||
|
)
|
||||||
|
|
||||||
|
form = ClientForm(obj=client)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
client.set_client_metadata(form.data)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for(".client_edit", client_id=client.id))
|
||||||
|
|
||||||
|
return render_template("client_edit.html", client=client, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
# OAuth API
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/oauth/authorize", methods=["GET", "POST"])
|
@bp.route("/oauth/authorize", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def authorize():
|
def authorize():
|
||||||
|
@ -76,6 +119,14 @@ def authorize():
|
||||||
grant = authorization.validate_consent_request(end_user=current_user)
|
grant = authorization.validate_consent_request(end_user=current_user)
|
||||||
except OAuth2Error as error:
|
except OAuth2Error as error:
|
||||||
return jsonify(dict(error.get_body()))
|
return jsonify(dict(error.get_body()))
|
||||||
|
|
||||||
|
if Token.query.filter(
|
||||||
|
Token.client_id == grant.client.client_id,
|
||||||
|
Token.user_id == current_user.get_user_id(),
|
||||||
|
).count():
|
||||||
|
# User has unrevoked token already - grant by default
|
||||||
|
return authorization.create_authorization_response(grant_user=current_user)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"oauthorize.html", user=current_user, grant=grant, client=grant.client
|
"oauthorize.html", user=current_user, grant=grant, client=grant.client
|
||||||
)
|
)
|
||||||
|
@ -99,7 +150,6 @@ def issue_token():
|
||||||
@require_oauth("profile:read")
|
@require_oauth("profile:read")
|
||||||
def api_profile():
|
def api_profile():
|
||||||
user = current_token.user
|
user = current_token.user
|
||||||
print(user.email, user.username, user.gecos, user.phone, user.personal_email)
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
email=user.email,
|
email=user.email,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
|
@ -129,7 +179,7 @@ def api_userinfo():
|
||||||
def openid_configuration():
|
def openid_configuration():
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"issuer": current_app.config['JWT_CONFIG']['iss'],
|
"issuer": current_app.config["JWT_CONFIG"]["iss"],
|
||||||
"authorization_endpoint": url_for(".authorize", _external=True),
|
"authorization_endpoint": url_for(".authorize", _external=True),
|
||||||
"token_endpoint": url_for(".issue_token", _external=True),
|
"token_endpoint": url_for(".issue_token", _external=True),
|
||||||
"userinfo_endpoint": url_for(".api_userinfo", _external=True),
|
"userinfo_endpoint": url_for(".api_userinfo", _external=True),
|
||||||
|
|
|
@ -27,3 +27,13 @@ td.placeholder {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 7px
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group li {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
{% macro format_currency(amount, color=True, precision=2) -%}
|
||||||
|
{%- if amount == None -%}
|
||||||
|
None
|
||||||
|
{%- else -%}
|
||||||
|
<span class="amount{% if color %}{% if amount < 0 %} amount-negative{% else %} amount-positive{% endif %}{% endif %}" data-original="{{ amount }}">
|
||||||
|
{{ format_currency_raw(amount, precision) }}
|
||||||
|
</span>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro format_currency_raw(amount, precision=0) -%}
|
||||||
|
{{ ("%%.%sf" | format(precision) | format(amount/100)) }}SOG
|
||||||
|
{%- endmacro %}
|
||||||
|
{% macro render_field(field, prefix=None, suffix=None, layout=True, label=True, split=3) %}
|
||||||
|
{% if field.type == 'HiddenField' or field.type == 'CSRFTokenField' %}
|
||||||
|
{{ field(**kwargs) }}
|
||||||
|
{% else %}
|
||||||
|
{% if layout %}
|
||||||
|
<div class="form-group{% if field.errors %} has-error{% endif %}">
|
||||||
|
{% if field.type == 'BooleanField' %}
|
||||||
|
<div class="col-md-{{ split }}"></div>
|
||||||
|
{% elif label %}
|
||||||
|
{{ field.label(class_='col-md-%s control-label' % (split,) + (' control-label-required' if field.flags.required else '')) }}
|
||||||
|
{% endif %}
|
||||||
|
<div class="col-md-{{ 12 - split }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ render_field_inner(field, prefix, suffix, label=label, **kwargs) }}
|
||||||
|
|
||||||
|
{% if layout %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_field_inner(field, prefix=None, suffix=None, label=True, input_group_class='') %}
|
||||||
|
{% if field.type == 'BooleanField' %}<div class="checkbox"><label for="{{ field.id }}">{% endif %}
|
||||||
|
{% if prefix or suffix %}<div class="input-group {{ input_group_class }}">{% endif %}
|
||||||
|
{% if prefix %}<span class="input-group-addon">{{ prefix }}</span>{% endif %}
|
||||||
|
{% if field.type == 'BooleanField' %}
|
||||||
|
{{ field(**kwargs) }} {% if label %}{{ field.label.text }}{% endif %}
|
||||||
|
{% elif field.type == 'RadioField' %}
|
||||||
|
{{ field(**kwargs) }}
|
||||||
|
{% elif field.type == 'MultiCheckboxField' %}
|
||||||
|
{{ field(**kwargs) }}
|
||||||
|
{% elif field.type == 'FieldList' %}
|
||||||
|
{% for f in field.entries %}
|
||||||
|
{{ render_field_inner(f) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{{ field(class_='form-control '+kwargs.pop('class_', ''), **kwargs) }}
|
||||||
|
{% endif %}
|
||||||
|
{% if suffix %}<span class="input-group-addon">{{ suffix }}</span>{% endif %}
|
||||||
|
{% if prefix or suffix %}</div>{% endif %}
|
||||||
|
{% if field.description and label %}
|
||||||
|
<span class="help-block">{{ field.description }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<span class="help-block">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% if field.type == 'BooleanField' %}</label></div>{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_submit(label='Submit', class_='btn btn-primary', layout=True) %}
|
||||||
|
{% if layout %}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="{{ class_ }}">{{ label }}</button>
|
||||||
|
{% if layout %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_pagination(pagination) %}
|
||||||
|
<ul class="pagination text-center">
|
||||||
|
{% if pagination.has_prev %}
|
||||||
|
<li><a href="{{ url_for_other_page(pagination.page - 1) }}">«</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a>«</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{%- for page in pagination.iter_pages() %}
|
||||||
|
{% if page %}
|
||||||
|
{% if page != pagination.page %}
|
||||||
|
<li><a href="{{ url_for_other_page(page) }}">{{ page }}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="active"><a href="{{ url_for_other_page(page) }}">{{ page }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a>…</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
{% if pagination.has_next %}
|
||||||
|
<li><a href="{{ url_for_other_page(pagination.page + 1) }}">»</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a>»</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endmacro %}
|
|
@ -0,0 +1,61 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% from "_helpers.html" import render_field, render_submit %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro static_field(id, label, value) %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label control-label-required" for="{{ id }}">{{ label }}</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<input class="form-control" id="{{ id }}" name="{{ id }}" required="" type="text" value="{{ value }}" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
<h2 class="page-header">
|
||||||
|
{% if client is defined %}Client edit{% else %}Client registration{% endif %}
|
||||||
|
</h2>
|
||||||
|
<form action="" class="form-horizontal" method="POST">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{{ render_field(form.client_name) }}
|
||||||
|
{{ render_field(form.client_uri) }}
|
||||||
|
{{ render_field(form.redirect_uris) }}
|
||||||
|
{{ render_field(form.token_endpoint_auth_method) }}
|
||||||
|
{{ render_field(form.grant_types) }}
|
||||||
|
{{ render_field(form.response_types) }}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if client is defined %}
|
||||||
|
{{ static_field('client_id', 'Client ID', client.client_id) }}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label control-label-required" for="client_secret">Client secret</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control" id="client_secret" name="client_secret" required="" type="password" value="{{ client.client_secret }}" readonly>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" data-toggle="#client_secret"><i class="glyphicon glyphicon-eye-open"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ static_field('openid_configuration', 'OpenID Connect Discovery Endpoint', url_for('.openid_configuration', _external=True)) }}
|
||||||
|
{{ static_field('token_endpoint', 'Token Endpoint', url_for('.issue_token', _external=True)) }}
|
||||||
|
{{ static_field('authorize_endpoint', 'Authorize Endpoint', url_for('.authorize', _external=True)) }}
|
||||||
|
{{ static_field('userinfo_endpoint', 'UserInfo Endpoint', url_for('.api_userinfo', _external=True)) }}
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('button[data-toggle]').forEach(e => {
|
||||||
|
e.addEventListener('click', () => {
|
||||||
|
const input = document.querySelector(e.attributes['data-toggle'].value);
|
||||||
|
input.type = (input.type === 'password') ? 'text' : 'password';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -40,6 +40,23 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h3>My applications <a href="{{ url_for('.client_create') }}" class="btn btn-primary btn-xs pull-right"><i class="glyphicon glyphicon-plus"></i> Register new application</a></h3>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Application name</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for client in clients %}
|
||||||
|
<tr><td>{{ client.client_name }}</td><td><a href="{{ url_for('.client_edit', client_id=client.id) }}" class="btn btn-xs btn-warning">Edit</a></td></tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan=4 class="placeholder">No registered applications yet</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in New Issue