Skip to content
Snippets Groups Projects
Commit 098f3828 authored by kaiyou's avatar kaiyou
Browse files

Naive OIDC implementation (lacks code persistence)

parent 82892b26
No related branches found
No related tags found
No related merge requests found
import os
import logging
logging.basicConfig(level=logging.DEBUG)
DEFAULT_CONFIG = {
......
{% extends "form.html" %}
{% set protocol_name = protocol.__name__ %}
{% block title %}{% trans %}Create a service{% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}add a SAML service{% endtrans %}{% endblock %}
{% block subtitle %}{% trans protocol_name %}add a {{ protocol_name }} service{% endtrans %}{% endblock %}
......@@ -8,3 +8,19 @@ class SAMLForm(flask_wtf.FlaskForm):
entityid = fields.StringField(_('SP entity id'), [validators.URL()])
acs = fields.StringField(_('SP ACS'), [validators.URL()])
submit = fields.SubmitField(_('Submit'))
class OIDCForm(flask_wtf.FlaskForm):
redirect_uri = fields.StringField(_("Redirect URI"), [validators.URL()])
token_endpoint_auth_method = fields.SelectField(
_('Token Endpoint Auth Method'), choices=[
("client_secret_post", _("HTTP POST data")),
("client_secret_basic", _("HTTP basic authorization"))
]
)
grant_type = fields.SelectField(
_('OpenID Connect grant type'), choices=[
("authorization_code", _("Authorization Code"))
]
)
submit = fields.SubmitField(_('Submit'))
from werkzeug.security import gen_salt
from authlib.flask import oauth2 as flask_oauth2
from authlib.flask.oauth2 import sqla as models_oauth2
from authlib.oauth2.rfc6749 import grants as grants_oauth2
from authlib.oidc import core as oidc
from hiboo.sso import forms, blueprint
from hiboo import models, utils, identity
import flask
AUTHORIZATION_CODES = {}
NONCES = {}
class Config(object):
""" Handles service configuration and forms.
"""
@classmethod
def derive_form(cls, form):
""" Add required fields to a form.
"""
return type('NewForm', (forms.OIDCForm, form), {})
@classmethod
def populate_service(cls, form, service):
""" Populate a service from a form
"""
service.config.update({
"token_endpoint_auth_method": form.token_endpoint_auth_method.data,
"redirect_uri": form.redirect_uri.data
})
cls.update_client(service)
@classmethod
def populate_form(cls, service, form):
""" Populate a form from a service
"""
form.token_endpoint_auth_method.data = config.get(
"token_endpoint_auth_method",
"client_secret_post"
)
form.redirect_uri.data = config.get("redirect_uri")
@classmethod
def update_client(cls, service):
if "client_id" not in service.config:
service.config["client_id"] = gen_salt(24)
service.config["client_secret"] = gen_salt(48)
service.config["jwt_key"] = gen_salt(24)
service.config["jwt_alg"] = "HS256"
class Client(models.db.Model, models_oauth2.OAuth2ClientMixin):
""" OIDC client that only supports authorization code
"""
scope = "openid"
grant_type = "authorization_code"
response_type = "code"
jwt_alg = models.db.Column(models.db.Text())
jwt_key = models.db.Column(models.db.Text())
def __init__(self, service):
self.service = service
super(Client, self).__init__(**service.config)
# The authorization server is specific to a client
self.authorization = flask_oauth2.AuthorizationServer(
query_client=self.query_client,
save_token=self.save_token,
app=flask.current_app
)
self.authorization.register_grant(
AuthorizationCodeGrant, [OpenIDCode(required_nonce=True)]
)
def query_client(self, client_id):
return self if client_id == self.client_id else None
def save_token(self, token, request):
# TODO: atm we do not save any token
pass
class AuthorizationCodeGrant(grants_oauth2.AuthorizationCodeGrant):
""" Authorization code grant
"""
def create_authorization_code(self, client, grant_user, request):
code = gen_salt(48) # TODO
nonce = request.data.get('nonce')
item = AuthorizationCode(
code=code,
client_id=client.client_id,
redirect_uri=request.redirect_uri,
scope=request.scope,
user_id=grant_user.uuid,
nonce=nonce
)
AUTHORIZATION_CODES[code] = item
NONCES[nonce] = item
return code
def parse_authorization_code(self, code, client):
code = AUTHORIZATION_CODES.get(code)
if code and code.client_id == client.client_id:
return code
def delete_authorization_code(self, authorization_code):
del AUTHORIZATION_CODES[authorization_code.code]
del NONCES[authorization_code.nonce]
def authenticate_user(self, authorization_code):
profile = models.Identity.query.get(authorization_code.user_id)
return profile
class AuthorizationCode(models.db.Model, models_oauth2.OIDCAuthorizationCodeMixin):
""" Authorization code object for storage
"""
user_id = models.db.Column(models.db.Text())
class OpenIDCode(oidc.grants.OpenIDCode):
""" Identity token
"""
def exists_nonce(self, nonce, request):
nonce = NONCES.get(nonce)
return nonce and nonce["client_id"] == request.client_id
def get_jwt_config(self, grant):
return { # TODO
'key': grant.client.jwt_key,
'alg': grant.client.jwt_alg,
'iss': flask.url_for("sso.oidc_token", service_uuid=grant.client.service.uuid),
'exp': 3600,
}
def generate_user_info(self, user, scope):
info = oidc.UserInfo(sub=user.uuid, name=user.username)
info["email"] = user.email
return info
@blueprint.route("/authorize/<service_uuid>", methods=["GET", "POST"])
def oidc_authorize(service_uuid):
# Get the profile from user input (implies redirects)
service = models.Service.query.get(service_uuid) or flask.abort(404)
service.protocol == "oidc" or flask.abort(404)
profile = identity.get_identity(service, intent=True) or flask.abort(403)
# Generate and return the response
client = Client(service)
return client.authorization.create_authorization_response(grant_user=profile)
@blueprint.route("/token/<service_uuid>", methods=["POST"])
def oidc_token(service_uuid):
# Get the profile from user input (implies redirects)
service = models.Service.query.get(service_uuid) or flask.abort(404)
service.protocol == "oidc" or flask.abort(404)
# Generate and return the response
client = Client(service)
return client.authorization.create_token_response()
{% macro name() %}OIDC{% endmacro %}
{% macro description() %}{% trans %}OpenID Connect (OIDC) is JWT based authentication and authorization protocol{% endtrans %}{% endmacro %}
{% macro describe(service) %}
<dt>{% trans %}Authorization endpoint{% endtrans %}</dt>
<dd>{{ url_for("sso.oidc_authorize", service_uuid=service.uuid, _external=True) }}</dd>
<dt>{% trans %}Token endpoint{% endtrans %}</dt>
<dd>{{ url_for("sso.oidc_token", service_uuid=service.uuid, _external=True) }}</dd>
<dt>{% trans %}Client ID{% endtrans %}</dt>
<dd><pre>{{ service.config["client_id"] }}</pre></dd>
<dt>{% trans %}Client secret{% endtrans %}</dt>
<dd><pre>{{ service.config["client_secret"] }}</dd>
{% endmacro %}
......@@ -15,3 +15,4 @@ bcrypt
pysaml2
xmlsec
cryptography
authlib
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment