forked from Github-Mirrors/canaille
split oidc code from the rest
This commit is contained in:
parent
16d2d71194
commit
52e802b34f
37 changed files with 290 additions and 269 deletions
|
@ -15,7 +15,7 @@ from flask_themer import FileSystemThemeLoader
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
from flask_themer import Themer
|
from flask_themer import Themer
|
||||||
|
|
||||||
from .oauth2utils import setup_oauth
|
from .oidc.oauth2utils import setup_oauth
|
||||||
|
|
||||||
|
|
||||||
def setup_config(app, config=None, validate=True):
|
def setup_config(app, config=None, validate=True):
|
||||||
|
@ -214,19 +214,15 @@ def setup_themer(app):
|
||||||
def setup_blueprints(app):
|
def setup_blueprints(app):
|
||||||
import canaille.account
|
import canaille.account
|
||||||
import canaille.admin
|
import canaille.admin
|
||||||
import canaille.consents
|
|
||||||
import canaille.groups
|
import canaille.groups
|
||||||
import canaille.oauth
|
import canaille.oidc
|
||||||
import canaille.well_known
|
|
||||||
|
|
||||||
app.url_map.strict_slashes = False
|
app.url_map.strict_slashes = False
|
||||||
|
|
||||||
app.register_blueprint(canaille.account.bp)
|
app.register_blueprint(canaille.account.bp)
|
||||||
app.register_blueprint(canaille.admin.bp, url_prefix="/admin")
|
app.register_blueprint(canaille.admin.bp)
|
||||||
app.register_blueprint(canaille.groups.bp, url_prefix="/groups")
|
app.register_blueprint(canaille.groups.bp)
|
||||||
app.register_blueprint(canaille.oauth.bp, url_prefix="/oauth")
|
app.register_blueprint(canaille.oidc.bp)
|
||||||
app.register_blueprint(canaille.consents.bp, url_prefix="/consent")
|
|
||||||
app.register_blueprint(canaille.well_known.bp, url_prefix="/.well-known")
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(config=None, validate=True):
|
def create_app(config=None, validate=True):
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
from . import authorizations
|
|
||||||
from . import clients
|
|
||||||
from . import mail
|
from . import mail
|
||||||
from . import tokens
|
|
||||||
|
|
||||||
bp = Blueprint("admin", __name__)
|
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||||
|
|
||||||
bp.register_blueprint(tokens.bp, url_prefix="/token")
|
bp.register_blueprint(mail.bp)
|
||||||
bp.register_blueprint(authorizations.bp, url_prefix="/authorization")
|
|
||||||
bp.register_blueprint(clients.bp, url_prefix="/client")
|
|
||||||
bp.register_blueprint(mail.bp, url_prefix="/mail")
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ from wtforms.validators import DataRequired
|
||||||
from wtforms.validators import Email
|
from wtforms.validators import Email
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("mails", __name__)
|
bp = Blueprint("mails", __name__, url_prefix="/mail")
|
||||||
|
|
||||||
|
|
||||||
class MailTestForm(FlaskForm):
|
class MailTestForm(FlaskForm):
|
||||||
|
@ -43,7 +43,7 @@ def mail_index(user):
|
||||||
else:
|
else:
|
||||||
flash(_("The test invitation mail has been sent correctly"), "error")
|
flash(_("The test invitation mail has been sent correctly"), "error")
|
||||||
|
|
||||||
return render_template("admin/mails.html", form=form)
|
return render_template("mail/admin.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/password-init.html")
|
@bp.route("/password-init.html")
|
||||||
|
|
|
@ -2,8 +2,8 @@ import sys
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from canaille import create_app
|
from canaille import create_app
|
||||||
from canaille.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from canaille.models import Token
|
from canaille.oidc.models import Token
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask.cli import FlaskGroup
|
from flask.cli import FlaskGroup
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
|
|
|
@ -13,7 +13,7 @@ from .forms import GroupForm
|
||||||
from .forms import unique_group
|
from .forms import unique_group
|
||||||
from .models import Group
|
from .models import Group
|
||||||
|
|
||||||
bp = Blueprint("groups", __name__)
|
bp = Blueprint("groups", __name__, url_prefix="/groups")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
|
|
|
@ -7,10 +7,10 @@ from cryptography.hazmat.backends import default_backend as crypto_default_backe
|
||||||
from cryptography.hazmat.primitives import serialization as crypto_serialization
|
from cryptography.hazmat.primitives import serialization as crypto_serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
from .models import AuthorizationCode
|
from .oidc.models import AuthorizationCode
|
||||||
from .models import Client
|
from .oidc.models import Client
|
||||||
from .models import Consent
|
from .oidc.models import Consent
|
||||||
from .models import Token
|
from .oidc.models import Token
|
||||||
|
|
||||||
|
|
||||||
class InstallationException(Exception):
|
class InstallationException(Exception):
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
import datetime
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import ldap.filter
|
import ldap.filter
|
||||||
from authlib.oauth2.rfc6749 import AuthorizationCodeMixin
|
|
||||||
from authlib.oauth2.rfc6749 import ClientMixin
|
|
||||||
from authlib.oauth2.rfc6749 import TokenMixin
|
|
||||||
from authlib.oauth2.rfc6749 import util
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import session
|
from flask import session
|
||||||
|
|
||||||
|
@ -229,176 +222,3 @@ class Group(LDAPObject):
|
||||||
def remove_member(self, user, conn=None):
|
def remove_member(self, user, conn=None):
|
||||||
self.member = [m for m in self.member if m != user.dn]
|
self.member = [m for m in self.member if m != user.dn]
|
||||||
self.save(conn=conn)
|
self.save(conn=conn)
|
||||||
|
|
||||||
|
|
||||||
class Client(LDAPObject, ClientMixin):
|
|
||||||
object_class = ["oauthClient"]
|
|
||||||
base = "ou=clients,ou=oauth"
|
|
||||||
id = "oauthClientID"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def issue_date(self):
|
|
||||||
return self.oauthIssueDate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def preconsent(self):
|
|
||||||
return self.oauthPreconsent
|
|
||||||
|
|
||||||
def get_client_id(self):
|
|
||||||
return self.oauthClientID
|
|
||||||
|
|
||||||
def get_default_redirect_uri(self):
|
|
||||||
return self.oauthRedirectURIs[0]
|
|
||||||
|
|
||||||
def get_allowed_scope(self, scope):
|
|
||||||
return util.list_to_scope(self.oauthScope)
|
|
||||||
|
|
||||||
def check_redirect_uri(self, redirect_uri):
|
|
||||||
return redirect_uri in self.oauthRedirectURIs
|
|
||||||
|
|
||||||
def has_client_secret(self):
|
|
||||||
return bool(self.oauthClientSecret)
|
|
||||||
|
|
||||||
def check_client_secret(self, client_secret):
|
|
||||||
return client_secret == self.oauthClientSecret
|
|
||||||
|
|
||||||
def check_token_endpoint_auth_method(self, method):
|
|
||||||
return method == self.oauthTokenEndpointAuthMethod
|
|
||||||
|
|
||||||
def check_response_type(self, response_type):
|
|
||||||
return all(r in self.oauthResponseType for r in response_type.split(" "))
|
|
||||||
|
|
||||||
def check_grant_type(self, grant_type):
|
|
||||||
return grant_type in self.oauthGrantType
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client_info(self):
|
|
||||||
return dict(
|
|
||||||
client_id=self.client_id,
|
|
||||||
client_secret=self.client_secret,
|
|
||||||
client_id_issued_at=self.client_id_issued_at,
|
|
||||||
client_secret_expires_at=self.client_secret_expires_at,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationCode(LDAPObject, AuthorizationCodeMixin):
|
|
||||||
object_class = ["oauthAuthorizationCode"]
|
|
||||||
base = "ou=authorizations,ou=oauth"
|
|
||||||
id = "oauthCode"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def issue_date(self):
|
|
||||||
return self.oauthIssueDate
|
|
||||||
|
|
||||||
def get_redirect_uri(self):
|
|
||||||
return self.oauthRedirectURI
|
|
||||||
|
|
||||||
def get_scope(self):
|
|
||||||
return self.oauthScope
|
|
||||||
|
|
||||||
def get_nonce(self):
|
|
||||||
return self.oauthNonce
|
|
||||||
|
|
||||||
def is_expired(self):
|
|
||||||
return (
|
|
||||||
self.oauthAuthorizationDate
|
|
||||||
+ datetime.timedelta(seconds=int(self.oauthAuthorizationLifetime))
|
|
||||||
< datetime.datetime.now()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_auth_time(self):
|
|
||||||
return int(
|
|
||||||
(
|
|
||||||
self.oauthAuthorizationDate - datetime.datetime(1970, 1, 1)
|
|
||||||
).total_seconds()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Token(LDAPObject, TokenMixin):
|
|
||||||
object_class = ["oauthToken"]
|
|
||||||
base = "ou=tokens,ou=oauth"
|
|
||||||
id = "oauthAccessToken"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def issue_date(self):
|
|
||||||
return self.oauthIssueDate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def expire_date(self):
|
|
||||||
return self.oauthIssueDate + datetime.timedelta(
|
|
||||||
seconds=int(self.oauthTokenLifetime)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def revoked(self):
|
|
||||||
return bool(self.oauthRevokationDate)
|
|
||||||
|
|
||||||
def get_client_id(self):
|
|
||||||
return Client.get(self.oauthClient).oauthClientID
|
|
||||||
|
|
||||||
def get_scope(self):
|
|
||||||
return " ".join(self.oauthScope)
|
|
||||||
|
|
||||||
def get_expires_in(self):
|
|
||||||
return int(self.oauthTokenLifetime)
|
|
||||||
|
|
||||||
def get_issued_at(self):
|
|
||||||
return int(
|
|
||||||
(self.oauthIssueDate - datetime.datetime(1970, 1, 1)).total_seconds()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_expires_at(self):
|
|
||||||
issue_timestamp = (
|
|
||||||
self.oauthIssueDate - datetime.datetime(1970, 1, 1)
|
|
||||||
).total_seconds()
|
|
||||||
return int(issue_timestamp) + int(self.oauthTokenLifetime)
|
|
||||||
|
|
||||||
def is_refresh_token_active(self):
|
|
||||||
if self.oauthRevokationDate:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.expire_date >= datetime.datetime.now()
|
|
||||||
|
|
||||||
def is_expired(self):
|
|
||||||
return (
|
|
||||||
self.oauthIssueDate
|
|
||||||
+ datetime.timedelta(seconds=int(self.oauthTokenLifetime))
|
|
||||||
< datetime.datetime.now()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Consent(LDAPObject):
|
|
||||||
object_class = ["oauthConsent"]
|
|
||||||
base = "ou=consents,ou=oauth"
|
|
||||||
id = "cn"
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if "cn" not in kwargs:
|
|
||||||
kwargs["cn"] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def issue_date(self):
|
|
||||||
return self.oauthIssueDate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def revokation_date(self):
|
|
||||||
return self.oauthRevokationDate
|
|
||||||
|
|
||||||
def revoke(self):
|
|
||||||
self.oauthRevokationDate = datetime.datetime.now()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
tokens = Token.filter(
|
|
||||||
oauthClient=self.oauthClient,
|
|
||||||
oauthSubject=self.oauthSubject,
|
|
||||||
)
|
|
||||||
for t in tokens:
|
|
||||||
if t.revoked or any(
|
|
||||||
scope not in t.oauthScope[0] for scope in self.oauthScope
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
t.oauthRevokationDate = self.oauthRevokationDate
|
|
||||||
t.save()
|
|
||||||
|
|
17
canaille/oidc/__init__.py
Normal file
17
canaille/oidc/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
from . import authorizations
|
||||||
|
from . import clients
|
||||||
|
from . import consents
|
||||||
|
from . import oauth
|
||||||
|
from . import tokens
|
||||||
|
from . import well_known
|
||||||
|
|
||||||
|
bp = Blueprint("oidc", __name__)
|
||||||
|
|
||||||
|
bp.register_blueprint(authorizations.bp)
|
||||||
|
bp.register_blueprint(clients.bp)
|
||||||
|
bp.register_blueprint(consents.bp)
|
||||||
|
bp.register_blueprint(oauth.bp)
|
||||||
|
bp.register_blueprint(well_known.bp)
|
||||||
|
bp.register_blueprint(tokens.bp)
|
|
@ -1,10 +1,10 @@
|
||||||
from canaille.flaskutils import permissions_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
from canaille.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("authorizations", __name__)
|
bp = Blueprint("authorizations", __name__, url_prefix="/admin/authorization")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
|
@ -12,7 +12,7 @@ bp = Blueprint("authorizations", __name__)
|
||||||
def index(user):
|
def index(user):
|
||||||
authorizations = AuthorizationCode.filter()
|
authorizations = AuthorizationCode.filter()
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin/authorization_list.html", authorizations=authorizations
|
"oidc/admin/authorization_list.html", authorizations=authorizations
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,5 +21,7 @@ def index(user):
|
||||||
def view(user, authorization_id):
|
def view(user, authorization_id):
|
||||||
authorization = AuthorizationCode.get(authorization_id)
|
authorization = AuthorizationCode.get(authorization_id)
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin/authorization_view.html", authorization=authorization, menuitem="admin"
|
"oidc/admin/authorization_view.html",
|
||||||
|
authorization=authorization,
|
||||||
|
menuitem="admin",
|
||||||
)
|
)
|
|
@ -2,7 +2,7 @@ import datetime
|
||||||
|
|
||||||
import wtforms
|
import wtforms
|
||||||
from canaille.flaskutils import permissions_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
from canaille.models import Client
|
from canaille.oidc.models import Client
|
||||||
from flask import abort
|
from flask import abort
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import flash
|
from flask import flash
|
||||||
|
@ -15,14 +15,16 @@ from flask_wtf import FlaskForm
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("clients", __name__)
|
bp = Blueprint("clients", __name__, url_prefix="/admin/client")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@permissions_needed("manage_oidc")
|
@permissions_needed("manage_oidc")
|
||||||
def index(user):
|
def index(user):
|
||||||
clients = Client.filter()
|
clients = Client.filter()
|
||||||
return render_template("admin/client_list.html", clients=clients, menuitem="admin")
|
return render_template(
|
||||||
|
"oidc/admin/client_list.html", clients=clients, menuitem="admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def client_audiences():
|
def client_audiences():
|
||||||
|
@ -138,14 +140,18 @@ def add(user):
|
||||||
form = ClientAdd(request.form or None)
|
form = ClientAdd(request.form or None)
|
||||||
|
|
||||||
if not request.form:
|
if not request.form:
|
||||||
return render_template("admin/client_add.html", form=form, menuitem="admin")
|
return render_template(
|
||||||
|
"oidc/admin/client_add.html", form=form, menuitem="admin"
|
||||||
|
)
|
||||||
|
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
flash(
|
flash(
|
||||||
_("The client has not been added. Please check your information."),
|
_("The client has not been added. Please check your information."),
|
||||||
"error",
|
"error",
|
||||||
)
|
)
|
||||||
return render_template("admin/client_add.html", form=form, menuitem="admin")
|
return render_template(
|
||||||
|
"oidc/admin/client_add.html", form=form, menuitem="admin"
|
||||||
|
)
|
||||||
|
|
||||||
client_id = gen_salt(24)
|
client_id = gen_salt(24)
|
||||||
client_id_issued_at = datetime.datetime.now()
|
client_id_issued_at = datetime.datetime.now()
|
||||||
|
@ -179,7 +185,7 @@ def add(user):
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect(url_for("admin.clients.edit", client_id=client_id))
|
return redirect(url_for("oidc.clients.edit", client_id=client_id))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/edit/<client_id>", methods=["GET", "POST"])
|
@bp.route("/edit/<client_id>", methods=["GET", "POST"])
|
||||||
|
@ -204,7 +210,7 @@ def client_edit(client_id):
|
||||||
|
|
||||||
if not request.form:
|
if not request.form:
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin/client_edit.html", form=form, client=client, menuitem="admin"
|
"oidc/admin/client_edit.html", form=form, client=client, menuitem="admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
|
@ -240,7 +246,7 @@ def client_edit(client_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin/client_edit.html", form=form, client=client, menuitem="admin"
|
"oidc/admin/client_edit.html", form=form, client=client, menuitem="admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -251,4 +257,4 @@ def client_delete(client_id):
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
client.delete()
|
client.delete()
|
||||||
return redirect(url_for("admin.clients.index"))
|
return redirect(url_for("oidc.clients.index"))
|
|
@ -1,6 +1,6 @@
|
||||||
from canaille.flaskutils import user_needed
|
from canaille.flaskutils import user_needed
|
||||||
from canaille.models import Client
|
from canaille.oidc.models import Client
|
||||||
from canaille.models import Consent
|
from canaille.oidc.models import Consent
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import flash
|
from flask import flash
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
|
@ -9,7 +9,7 @@ from flask_babel import gettext
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("consents", __name__)
|
bp = Blueprint("consents", __name__, url_prefix="/consent")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
|
@ -20,7 +20,10 @@ def consents(user):
|
||||||
client_dns = list({t.oauthClient for t in consents})
|
client_dns = list({t.oauthClient for t in consents})
|
||||||
clients = {dn: Client.get(dn) for dn in client_dns}
|
clients = {dn: Client.get(dn) for dn in client_dns}
|
||||||
return render_template(
|
return render_template(
|
||||||
"consent_list.html", consents=consents, clients=clients, menuitem="consents"
|
"oidc/user/consent_list.html",
|
||||||
|
consents=consents,
|
||||||
|
clients=clients,
|
||||||
|
menuitem="consents",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,4 +39,4 @@ def delete(user, consent_id):
|
||||||
consent.revoke()
|
consent.revoke()
|
||||||
flash(gettext("The access has been revoked"), "success")
|
flash(gettext("The access has been revoked"), "success")
|
||||||
|
|
||||||
return redirect(url_for("consents.consents"))
|
return redirect(url_for("oidc.consents.consents"))
|
182
canaille/oidc/models.py
Normal file
182
canaille/oidc/models.py
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from authlib.oauth2.rfc6749 import AuthorizationCodeMixin
|
||||||
|
from authlib.oauth2.rfc6749 import ClientMixin
|
||||||
|
from authlib.oauth2.rfc6749 import TokenMixin
|
||||||
|
from authlib.oauth2.rfc6749 import util
|
||||||
|
|
||||||
|
from ..ldaputils import LDAPObject
|
||||||
|
|
||||||
|
|
||||||
|
class Client(LDAPObject, ClientMixin):
|
||||||
|
object_class = ["oauthClient"]
|
||||||
|
base = "ou=clients,ou=oauth"
|
||||||
|
id = "oauthClientID"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issue_date(self):
|
||||||
|
return self.oauthIssueDate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preconsent(self):
|
||||||
|
return self.oauthPreconsent
|
||||||
|
|
||||||
|
def get_client_id(self):
|
||||||
|
return self.oauthClientID
|
||||||
|
|
||||||
|
def get_default_redirect_uri(self):
|
||||||
|
return self.oauthRedirectURIs[0]
|
||||||
|
|
||||||
|
def get_allowed_scope(self, scope):
|
||||||
|
return util.list_to_scope(self.oauthScope)
|
||||||
|
|
||||||
|
def check_redirect_uri(self, redirect_uri):
|
||||||
|
return redirect_uri in self.oauthRedirectURIs
|
||||||
|
|
||||||
|
def has_client_secret(self):
|
||||||
|
return bool(self.oauthClientSecret)
|
||||||
|
|
||||||
|
def check_client_secret(self, client_secret):
|
||||||
|
return client_secret == self.oauthClientSecret
|
||||||
|
|
||||||
|
def check_token_endpoint_auth_method(self, method):
|
||||||
|
return method == self.oauthTokenEndpointAuthMethod
|
||||||
|
|
||||||
|
def check_response_type(self, response_type):
|
||||||
|
return all(r in self.oauthResponseType for r in response_type.split(" "))
|
||||||
|
|
||||||
|
def check_grant_type(self, grant_type):
|
||||||
|
return grant_type in self.oauthGrantType
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_info(self):
|
||||||
|
return dict(
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
client_id_issued_at=self.client_id_issued_at,
|
||||||
|
client_secret_expires_at=self.client_secret_expires_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationCode(LDAPObject, AuthorizationCodeMixin):
|
||||||
|
object_class = ["oauthAuthorizationCode"]
|
||||||
|
base = "ou=authorizations,ou=oauth"
|
||||||
|
id = "oauthCode"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issue_date(self):
|
||||||
|
return self.oauthIssueDate
|
||||||
|
|
||||||
|
def get_redirect_uri(self):
|
||||||
|
return self.oauthRedirectURI
|
||||||
|
|
||||||
|
def get_scope(self):
|
||||||
|
return self.oauthScope
|
||||||
|
|
||||||
|
def get_nonce(self):
|
||||||
|
return self.oauthNonce
|
||||||
|
|
||||||
|
def is_expired(self):
|
||||||
|
return (
|
||||||
|
self.oauthAuthorizationDate
|
||||||
|
+ datetime.timedelta(seconds=int(self.oauthAuthorizationLifetime))
|
||||||
|
< datetime.datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_auth_time(self):
|
||||||
|
return int(
|
||||||
|
(
|
||||||
|
self.oauthAuthorizationDate - datetime.datetime(1970, 1, 1)
|
||||||
|
).total_seconds()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Token(LDAPObject, TokenMixin):
|
||||||
|
object_class = ["oauthToken"]
|
||||||
|
base = "ou=tokens,ou=oauth"
|
||||||
|
id = "oauthAccessToken"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issue_date(self):
|
||||||
|
return self.oauthIssueDate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expire_date(self):
|
||||||
|
return self.oauthIssueDate + datetime.timedelta(
|
||||||
|
seconds=int(self.oauthTokenLifetime)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def revoked(self):
|
||||||
|
return bool(self.oauthRevokationDate)
|
||||||
|
|
||||||
|
def get_client_id(self):
|
||||||
|
return Client.get(self.oauthClient).oauthClientID
|
||||||
|
|
||||||
|
def get_scope(self):
|
||||||
|
return " ".join(self.oauthScope)
|
||||||
|
|
||||||
|
def get_expires_in(self):
|
||||||
|
return int(self.oauthTokenLifetime)
|
||||||
|
|
||||||
|
def get_issued_at(self):
|
||||||
|
return int(
|
||||||
|
(self.oauthIssueDate - datetime.datetime(1970, 1, 1)).total_seconds()
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_expires_at(self):
|
||||||
|
issue_timestamp = (
|
||||||
|
self.oauthIssueDate - datetime.datetime(1970, 1, 1)
|
||||||
|
).total_seconds()
|
||||||
|
return int(issue_timestamp) + int(self.oauthTokenLifetime)
|
||||||
|
|
||||||
|
def is_refresh_token_active(self):
|
||||||
|
if self.oauthRevokationDate:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.expire_date >= datetime.datetime.now()
|
||||||
|
|
||||||
|
def is_expired(self):
|
||||||
|
return (
|
||||||
|
self.oauthIssueDate
|
||||||
|
+ datetime.timedelta(seconds=int(self.oauthTokenLifetime))
|
||||||
|
< datetime.datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Consent(LDAPObject):
|
||||||
|
object_class = ["oauthConsent"]
|
||||||
|
base = "ou=consents,ou=oauth"
|
||||||
|
id = "cn"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "cn" not in kwargs:
|
||||||
|
kwargs["cn"] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issue_date(self):
|
||||||
|
return self.oauthIssueDate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def revokation_date(self):
|
||||||
|
return self.oauthRevokationDate
|
||||||
|
|
||||||
|
def revoke(self):
|
||||||
|
self.oauthRevokationDate = datetime.datetime.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
tokens = Token.filter(
|
||||||
|
oauthClient=self.oauthClient,
|
||||||
|
oauthSubject=self.oauthSubject,
|
||||||
|
)
|
||||||
|
for t in tokens:
|
||||||
|
if t.revoked or any(
|
||||||
|
scope not in t.oauthScope[0] for scope in self.oauthScope
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
t.oauthRevokationDate = self.oauthRevokationDate
|
||||||
|
t.save()
|
|
@ -15,11 +15,11 @@ from flask_babel import gettext
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
|
|
||||||
from .flaskutils import current_user
|
from ..flaskutils import current_user
|
||||||
from .forms import FullLoginForm
|
from ..forms import FullLoginForm
|
||||||
|
from ..models import User
|
||||||
from .models import Client
|
from .models import Client
|
||||||
from .models import Consent
|
from .models import Consent
|
||||||
from .models import User
|
|
||||||
from .oauth2utils import authorization
|
from .oauth2utils import authorization
|
||||||
from .oauth2utils import DEFAULT_JWT_ALG
|
from .oauth2utils import DEFAULT_JWT_ALG
|
||||||
from .oauth2utils import DEFAULT_JWT_KTY
|
from .oauth2utils import DEFAULT_JWT_KTY
|
||||||
|
@ -29,7 +29,7 @@ from .oauth2utils import require_oauth
|
||||||
from .oauth2utils import RevocationEndpoint
|
from .oauth2utils import RevocationEndpoint
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("oauth", __name__)
|
bp = Blueprint("oauth", __name__, url_prefix="/oauth")
|
||||||
|
|
||||||
CLAIMS = {
|
CLAIMS = {
|
||||||
"profile": (
|
"profile": (
|
||||||
|
@ -110,7 +110,7 @@ def authorize():
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"authorize.html",
|
"oidc/user/authorize.html",
|
||||||
user=user,
|
user=user,
|
||||||
grant=grant,
|
grant=grant,
|
||||||
client=client,
|
client=client,
|
|
@ -21,11 +21,11 @@ from authlib.oidc.core.grants import OpenIDHybridGrant as _OpenIDHybridGrant
|
||||||
from authlib.oidc.core.grants import OpenIDImplicitGrant as _OpenIDImplicitGrant
|
from authlib.oidc.core.grants import OpenIDImplicitGrant as _OpenIDImplicitGrant
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
from ..models import Group
|
||||||
|
from ..models import User
|
||||||
from .models import AuthorizationCode
|
from .models import AuthorizationCode
|
||||||
from .models import Client
|
from .models import Client
|
||||||
from .models import Group
|
|
||||||
from .models import Token
|
from .models import Token
|
||||||
from .models import User
|
|
||||||
|
|
||||||
DEFAULT_JWT_KTY = "RSA"
|
DEFAULT_JWT_KTY = "RSA"
|
||||||
DEFAULT_JWT_ALG = "RS256"
|
DEFAULT_JWT_ALG = "RS256"
|
|
@ -1,21 +1,23 @@
|
||||||
from canaille.flaskutils import permissions_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
from canaille.models import Token
|
from canaille.oidc.models import Token
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("tokens", __name__)
|
bp = Blueprint("tokens", __name__, url_prefix="/admin/token")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@permissions_needed("manage_oidc")
|
@permissions_needed("manage_oidc")
|
||||||
def index(user):
|
def index(user):
|
||||||
tokens = Token.filter()
|
tokens = Token.filter()
|
||||||
return render_template("admin/token_list.html", tokens=tokens, menuitem="admin")
|
return render_template(
|
||||||
|
"oidc/admin/token_list.html", tokens=tokens, menuitem="admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<token_id>", methods=["GET", "POST"])
|
@bp.route("/<token_id>", methods=["GET", "POST"])
|
||||||
@permissions_needed("manage_oidc")
|
@permissions_needed("manage_oidc")
|
||||||
def view(user, token_id):
|
def view(user, token_id):
|
||||||
token = Token.get(token_id)
|
token = Token.get(token_id)
|
||||||
return render_template("admin/token_view.html", token=token, menuitem="admin")
|
return render_template("oidc/admin/token_view.html", token=token, menuitem="admin")
|
|
@ -5,7 +5,7 @@ from flask import current_app
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("home", __name__)
|
bp = Blueprint("home", __name__, url_prefix="/.well-known")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/oauth-authorization-server")
|
@bp.route("/oauth-authorization-server")
|
|
@ -22,8 +22,8 @@
|
||||||
</thead>
|
</thead>
|
||||||
{% for authorization in authorizations %}
|
{% for authorization in authorizations %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for('admin.authorizations.view', authorization_id=authorization.oauthCode) }}">{{ authorization.oauthCode }}</a></td>
|
<td><a href="{{ url_for('oidc.authorizations.view', authorization_id=authorization.oauthCode) }}">{{ authorization.oauthCode }}</a></td>
|
||||||
<td><a href="{{ url_for('admin.clients.edit', client_id=authorization.oauthClientID) }}">{{ authorization.oauthClientID }}</a></td>
|
<td><a href="{{ url_for('oidc.clients.edit', client_id=authorization.oauthClientID) }}">{{ authorization.oauthClientID }}</a></td>
|
||||||
<td>{{ authorization.oauthSubject }}</td>
|
<td>{{ authorization.oauthSubject }}</td>
|
||||||
<td>{{ authorization.issue_date }}</td>
|
<td>{{ authorization.issue_date }}</td>
|
||||||
</tr>
|
</tr>
|
|
@ -14,7 +14,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
<a class="ui primary button" href="{{ url_for('admin.clients.add') }}">{% trans %}Add client{% endtrans %}</a>
|
<a class="ui primary button" href="{{ url_for('oidc.clients.add') }}">{% trans %}Add client{% endtrans %}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="ui table">
|
<table class="ui table">
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
{% for client in clients %}
|
{% for client in clients %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('admin.clients.edit', client_id=client.oauthClientID) }}">
|
<a href="{{ url_for('oidc.clients.edit', client_id=client.oauthClientID) }}">
|
||||||
{% if client.oauthLogoURI %}
|
{% if client.oauthLogoURI %}
|
||||||
<img class="ui avatar image" src="{{ client.oauthLogoURI }}" alt="Client logo">
|
<img class="ui avatar image" src="{{ client.oauthLogoURI }}" alt="Client logo">
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{{ url_for('admin.clients.edit', client_id=client.oauthClientID) }}">{{ client.oauthClientName }}</a></td>
|
<td><a href="{{ url_for('oidc.clients.edit', client_id=client.oauthClientID) }}">{{ client.oauthClientName }}</a></td>
|
||||||
<td><a href="{{ client.oauthClientURI }}">{{ client.oauthClientURI }}</a></td>
|
<td><a href="{{ client.oauthClientURI }}">{{ client.oauthClientURI }}</a></td>
|
||||||
<td>{% if client.issue_date %}{{ client.issue_date }}{% endif %}</td>
|
<td>{% if client.issue_date %}{{ client.issue_date }}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
|
@ -22,8 +22,8 @@
|
||||||
</thead>
|
</thead>
|
||||||
{% for token in tokens %}
|
{% for token in tokens %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for('admin.tokens.view', token_id=token.oauthAccessToken) }}">{{ token.oauthAccessToken }}</a></td>
|
<td><a href="{{ url_for('oidc.tokens.view', token_id=token.oauthAccessToken) }}">{{ token.oauthAccessToken }}</a></td>
|
||||||
<td><a href="{{ url_for('admin.clients.edit', client_id=token.oauthClientID) }}">{{ token.oauthClientID }}</a></td>
|
<td><a href="{{ url_for('oidc.clients.edit', client_id=token.oauthClientID) }}">{{ token.oauthClientID }}</a></td>
|
||||||
<td>{{ token.oauthSubject }}</td>
|
<td>{{ token.oauthSubject }}</td>
|
||||||
<td>{{ token.issue_date }}</td>
|
<td>{{ token.issue_date }}</td>
|
||||||
</tr>
|
</tr>
|
|
@ -49,7 +49,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="ui bottom attached button" href="{{ url_for('consents.delete', consent_id=consent.cn[0] ) }}">
|
<a class="ui bottom attached button" href="{{ url_for('oidc.consents.delete', consent_id=consent.cn[0] ) }}">
|
||||||
<i class="remove icon"></i>
|
<i class="remove icon"></i>
|
||||||
{% trans %}Remove access{% endtrans %}
|
{% trans %}Remove access{% endtrans %}
|
||||||
</a>
|
</a>
|
|
@ -40,7 +40,7 @@
|
||||||
</a>
|
</a>
|
||||||
{% if user.can_use_oidc %}
|
{% if user.can_use_oidc %}
|
||||||
<a class="item {% if menuitem == "consents" %}active{% endif %}"
|
<a class="item {% if menuitem == "consents" %}active{% endif %}"
|
||||||
href="{{ url_for('consents.consents') }}">
|
href="{{ url_for('oidc.consents.consents') }}">
|
||||||
<i class="handshake icon"></i>
|
<i class="handshake icon"></i>
|
||||||
{% trans %}My consents{% endtrans %}
|
{% trans %}My consents{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
|
@ -64,15 +64,15 @@
|
||||||
<i class="settings icon"></i>
|
<i class="settings icon"></i>
|
||||||
Admin
|
Admin
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="item" href="{{ url_for('admin.clients.index') }}">
|
<a class="item" href="{{ url_for('oidc.clients.index') }}">
|
||||||
<i class="plug icon"></i>
|
<i class="plug icon"></i>
|
||||||
{% trans %}Clients{% endtrans %}
|
{% trans %}Clients{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a class="item" href="{{ url_for('admin.tokens.index') }}">
|
<a class="item" href="{{ url_for('oidc.tokens.index') }}">
|
||||||
<i class="key icon"></i>
|
<i class="key icon"></i>
|
||||||
{% trans %}Tokens{% endtrans %}
|
{% trans %}Tokens{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a class="item" href="{{ url_for('admin.authorizations.index') }}">
|
<a class="item" href="{{ url_for('oidc.authorizations.index') }}">
|
||||||
<i class="user secret icon"></i>
|
<i class="user secret icon"></i>
|
||||||
{% trans %}Codes{% endtrans %}
|
{% trans %}Codes{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
|
|
6
tests/fixtures/themes/test/base.html
vendored
6
tests/fixtures/themes/test/base.html
vendored
|
@ -62,15 +62,15 @@
|
||||||
<i class="settings icon"></i>
|
<i class="settings icon"></i>
|
||||||
Admin
|
Admin
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="item" href="{{ url_for('admin.clients.index') }}">
|
<a class="item" href="{{ url_for('oidc.clients.index') }}">
|
||||||
<i class="plug icon"></i>
|
<i class="plug icon"></i>
|
||||||
{% trans %}Clients{% endtrans %}
|
{% trans %}Clients{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a class="item" href="{{ url_for('admin.tokens.index') }}">
|
<a class="item" href="{{ url_for('oidc.tokens.index') }}">
|
||||||
<i class="key icon"></i>
|
<i class="key icon"></i>
|
||||||
{% trans %}Tokens{% endtrans %}
|
{% trans %}Tokens{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<a class="item" href="{{ url_for('admin.authorizations.index') }}">
|
<a class="item" href="{{ url_for('oidc.authorizations.index') }}">
|
||||||
<i class="user secret icon"></i>
|
<i class="user secret icon"></i>
|
||||||
{% trans %}Codes{% endtrans %}
|
{% trans %}Codes{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from canaille.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from canaille.models import Client
|
from canaille.oidc.models import Client
|
||||||
from canaille.models import Consent
|
from canaille.oidc.models import Consent
|
||||||
from canaille.models import Token
|
from canaille.oidc.models import Token
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ from urllib.parse import urlsplit
|
||||||
|
|
||||||
from authlib.jose import jwt
|
from authlib.jose import jwt
|
||||||
from authlib.oauth2.rfc7636 import create_s256_code_challenge
|
from authlib.oauth2.rfc7636 import create_s256_code_challenge
|
||||||
from canaille.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from canaille.models import Consent
|
from canaille.oidc.models import Consent
|
||||||
from canaille.models import Token
|
from canaille.oidc.models import Token
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
from . import client_credentials
|
from . import client_credentials
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from canaille.commands import cli
|
from canaille.commands import cli
|
||||||
from canaille.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from canaille.models import Token
|
from canaille.oidc.models import Token
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from canaille.models import Client
|
from canaille.oidc.models import Client
|
||||||
|
|
||||||
|
|
||||||
def test_no_logged_no_access(testclient):
|
def test_no_logged_no_access(testclient):
|
||||||
|
|
|
@ -2,9 +2,9 @@ from urllib.parse import parse_qs
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from authlib.jose import jwt
|
from authlib.jose import jwt
|
||||||
from canaille.models import AuthorizationCode
|
|
||||||
from canaille.models import Token
|
|
||||||
from canaille.models import User
|
from canaille.models import User
|
||||||
|
from canaille.oidc.models import AuthorizationCode
|
||||||
|
from canaille.oidc.models import Token
|
||||||
|
|
||||||
|
|
||||||
def test_oauth_hybrid(testclient, slapd_connection, user, client):
|
def test_oauth_hybrid(testclient, slapd_connection, user, client):
|
||||||
|
|
|
@ -2,7 +2,7 @@ from urllib.parse import parse_qs
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from authlib.jose import jwt
|
from authlib.jose import jwt
|
||||||
from canaille.models import Token
|
from canaille.oidc.models import Token
|
||||||
|
|
||||||
|
|
||||||
def test_oauth_implicit(testclient, slapd_connection, user, client):
|
def test_oauth_implicit(testclient, slapd_connection, user, client):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from canaille.models import User
|
from canaille.models import User
|
||||||
from canaille.oauth2utils import generate_user_claims
|
from canaille.oidc.oauth2utils import generate_user_claims
|
||||||
|
|
||||||
STANDARD_CLAIMS = [
|
STANDARD_CLAIMS = [
|
||||||
"sub",
|
"sub",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from canaille.models import Token
|
from canaille.oidc.models import Token
|
||||||
|
|
||||||
from . import client_credentials
|
from . import client_credentials
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from canaille.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from canaille.models import Client
|
from canaille.oidc.models import Token
|
||||||
from canaille.models import Token
|
|
||||||
|
|
||||||
from . import client_credentials
|
from . import client_credentials
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue