add support for response_mode(query and fragment), nonce
This commit is contained in:
parent
e563dff496
commit
78e274addf
|
@ -1,3 +1,7 @@
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from jwcrypto import jwk, jwt
|
from jwcrypto import jwk, jwt
|
||||||
|
|
||||||
|
@ -6,14 +10,19 @@ from app.log import LOG
|
||||||
from app.models import ClientUser
|
from app.models import ClientUser
|
||||||
|
|
||||||
with open(OPENID_PRIVATE_KEY_PATH, "rb") as f:
|
with open(OPENID_PRIVATE_KEY_PATH, "rb") as f:
|
||||||
key = jwk.JWK.from_pem(f.read())
|
_key = jwk.JWK.from_pem(f.read())
|
||||||
|
|
||||||
|
|
||||||
def get_jwk_key() -> dict:
|
def get_jwk_key() -> dict:
|
||||||
return key._public_params()
|
return _key._public_params()
|
||||||
|
|
||||||
|
|
||||||
def make_id_token(client_user: ClientUser):
|
def make_id_token(
|
||||||
|
client_user: ClientUser,
|
||||||
|
nonce: Optional[str] = None,
|
||||||
|
access_token: Optional[str] = None,
|
||||||
|
code: Optional[str] = None,
|
||||||
|
):
|
||||||
"""Make id_token for OpenID Connect
|
"""Make id_token for OpenID Connect
|
||||||
According to RFC 7519, these claims are mandatory:
|
According to RFC 7519, these claims are mandatory:
|
||||||
- iss
|
- iss
|
||||||
|
@ -31,20 +40,42 @@ def make_id_token(client_user: ClientUser):
|
||||||
"auth_time": arrow.now().timestamp,
|
"auth_time": arrow.now().timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nonce:
|
||||||
|
claims["nonce"] = nonce
|
||||||
|
|
||||||
|
if access_token:
|
||||||
|
claims["at_hash"] = id_token_hash(access_token)
|
||||||
|
|
||||||
|
if code:
|
||||||
|
claims["c_hash"] = id_token_hash(code)
|
||||||
|
|
||||||
claims = {**claims, **client_user.get_user_info()}
|
claims = {**claims, **client_user.get_user_info()}
|
||||||
|
|
||||||
jwt_token = jwt.JWT(
|
jwt_token = jwt.JWT(
|
||||||
header={"alg": "RS256", "kid": key._public_params()["kid"]}, claims=claims
|
header={"alg": "RS256", "kid": _key._public_params()["kid"]}, claims=claims
|
||||||
)
|
)
|
||||||
jwt_token.make_signed_token(key)
|
jwt_token.make_signed_token(_key)
|
||||||
return jwt_token.serialize()
|
return jwt_token.serialize()
|
||||||
|
|
||||||
|
|
||||||
def verify_id_token(id_token) -> bool:
|
def verify_id_token(id_token) -> bool:
|
||||||
try:
|
try:
|
||||||
jwt.JWT(key=key, jwt=id_token)
|
jwt.JWT(key=_key, jwt=id_token)
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception("id token not verified")
|
LOG.exception("id token not verified")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def decode_id_token(id_token) -> jwt.JWT:
|
||||||
|
return jwt.JWT(key=_key, jwt=id_token)
|
||||||
|
|
||||||
|
|
||||||
|
def id_token_hash(value, hashfunc=hashlib.sha256):
|
||||||
|
"""
|
||||||
|
Inspired from oauthlib
|
||||||
|
"""
|
||||||
|
digest = hashfunc(value.encode()).digest()
|
||||||
|
left_most = len(digest) // 2
|
||||||
|
return base64.urlsafe_b64encode(digest[:left_most]).decode().rstrip("=")
|
||||||
|
|
|
@ -18,7 +18,14 @@ from app.models import (
|
||||||
OauthToken,
|
OauthToken,
|
||||||
)
|
)
|
||||||
from app.oauth.base import oauth_bp
|
from app.oauth.base import oauth_bp
|
||||||
from app.oauth_models import get_response_types, ResponseType, Scope
|
from app.oauth_models import (
|
||||||
|
get_response_types,
|
||||||
|
ResponseType,
|
||||||
|
Scope,
|
||||||
|
SUPPORTED_OPENID_FLOWS,
|
||||||
|
SUPPORTED_OPENID_FLOWS_STR,
|
||||||
|
response_types_to_str,
|
||||||
|
)
|
||||||
from app.utils import random_string, encode_url, convert_to_id
|
from app.utils import random_string, encode_url, convert_to_id
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +42,8 @@ def authorize():
|
||||||
state = request.args.get("state")
|
state = request.args.get("state")
|
||||||
scope = request.args.get("scope")
|
scope = request.args.get("scope")
|
||||||
redirect_uri = request.args.get("redirect_uri")
|
redirect_uri = request.args.get("redirect_uri")
|
||||||
|
response_mode = request.args.get("response_mode")
|
||||||
|
nonce = request.args.get("nonce")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response_types: [ResponseType] = get_response_types(request)
|
response_types: [ResponseType] = get_response_types(request)
|
||||||
|
@ -45,6 +54,12 @@ def authorize():
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if set(response_types) not in SUPPORTED_OPENID_FLOWS:
|
||||||
|
return (
|
||||||
|
f"SimpleLogin only support the following OIDC flows: {SUPPORTED_OPENID_FLOWS_STR}",
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
if not redirect_uri:
|
if not redirect_uri:
|
||||||
LOG.d("no redirect uri")
|
LOG.d("no redirect uri")
|
||||||
return "redirect_uri must be set", 400
|
return "redirect_uri must be set", 400
|
||||||
|
@ -191,8 +206,8 @@ def authorize():
|
||||||
if scope:
|
if scope:
|
||||||
redirect_args["scope"] = scope
|
redirect_args["scope"] = scope
|
||||||
|
|
||||||
for response_type in response_types:
|
auth_code = None
|
||||||
if response_type == ResponseType.CODE:
|
if ResponseType.CODE in response_types:
|
||||||
# Create authorization code
|
# Create authorization code
|
||||||
auth_code = AuthorizationCode.create(
|
auth_code = AuthorizationCode.create(
|
||||||
client_id=client.id,
|
client_id=client.id,
|
||||||
|
@ -200,10 +215,13 @@ def authorize():
|
||||||
code=random_string(),
|
code=random_string(),
|
||||||
scope=scope,
|
scope=scope,
|
||||||
redirect_uri=redirect_uri,
|
redirect_uri=redirect_uri,
|
||||||
|
response_type=response_types_to_str(response_types),
|
||||||
)
|
)
|
||||||
db.session.add(auth_code)
|
db.session.add(auth_code)
|
||||||
redirect_args["code"] = auth_code.code
|
redirect_args["code"] = auth_code.code
|
||||||
elif response_type == ResponseType.TOKEN:
|
|
||||||
|
oauth_token = None
|
||||||
|
if ResponseType.TOKEN in response_types:
|
||||||
# create access-token
|
# create access-token
|
||||||
oauth_token = OauthToken.create(
|
oauth_token = OauthToken.create(
|
||||||
client_id=client.id,
|
client_id=client.id,
|
||||||
|
@ -211,16 +229,35 @@ def authorize():
|
||||||
scope=scope,
|
scope=scope,
|
||||||
redirect_uri=redirect_uri,
|
redirect_uri=redirect_uri,
|
||||||
access_token=generate_access_token(),
|
access_token=generate_access_token(),
|
||||||
|
response_type=response_types_to_str(response_types),
|
||||||
)
|
)
|
||||||
db.session.add(oauth_token)
|
db.session.add(oauth_token)
|
||||||
redirect_args["access_token"] = oauth_token.access_token
|
redirect_args["access_token"] = oauth_token.access_token
|
||||||
elif response_type == ResponseType.ID_TOKEN:
|
|
||||||
redirect_args["id_token"] = make_id_token(client_user)
|
if ResponseType.ID_TOKEN in response_types:
|
||||||
|
redirect_args["id_token"] = make_id_token(
|
||||||
|
client_user,
|
||||||
|
nonce,
|
||||||
|
oauth_token.access_token if oauth_token else None,
|
||||||
|
auth_code.code if auth_code else None,
|
||||||
|
)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
# should all params appended the url using fragment (#) or query
|
||||||
|
fragment = False
|
||||||
|
|
||||||
|
if response_mode and response_mode == "fragment":
|
||||||
|
fragment = True
|
||||||
|
|
||||||
|
# if response_types contain "token" => implicit flow => should use fragment
|
||||||
|
# except if client sets explicitly response_mode
|
||||||
|
if not response_mode:
|
||||||
|
if ResponseType.TOKEN in response_types:
|
||||||
|
fragment = True
|
||||||
|
|
||||||
# construct redirect_uri with redirect_args
|
# construct redirect_uri with redirect_args
|
||||||
return redirect(construct_url(redirect_uri, redirect_args))
|
return redirect(construct_url(redirect_uri, redirect_args, fragment))
|
||||||
|
|
||||||
|
|
||||||
def create_or_choose_gen_email(user) -> GenEmail:
|
def create_or_choose_gen_email(user) -> GenEmail:
|
||||||
|
@ -238,12 +275,15 @@ def create_or_choose_gen_email(user) -> GenEmail:
|
||||||
return gen_email
|
return gen_email
|
||||||
|
|
||||||
|
|
||||||
def construct_url(url, args: Dict[str, str]):
|
def construct_url(url, args: Dict[str, str], fragment: bool = False):
|
||||||
for i, (k, v) in enumerate(args.items()):
|
for i, (k, v) in enumerate(args.items()):
|
||||||
# make sure to escape v
|
# make sure to escape v
|
||||||
v = encode_url(v)
|
v = encode_url(v)
|
||||||
|
|
||||||
if i == 0:
|
if i == 0:
|
||||||
|
if fragment:
|
||||||
|
url += f"#{k}={v}"
|
||||||
|
else:
|
||||||
url += f"?{k}={v}"
|
url += f"?{k}={v}"
|
||||||
else:
|
else:
|
||||||
url += f"&{k}={v}"
|
url += f"&{k}={v}"
|
||||||
|
|
|
@ -7,7 +7,7 @@ from app.log import LOG
|
||||||
from app.models import Client, AuthorizationCode, OauthToken, ClientUser
|
from app.models import Client, AuthorizationCode, OauthToken, ClientUser
|
||||||
from app.oauth.base import oauth_bp
|
from app.oauth.base import oauth_bp
|
||||||
from app.oauth.views.authorize import generate_access_token
|
from app.oauth.views.authorize import generate_access_token
|
||||||
from app.oauth_models import Scope
|
from app.oauth_models import Scope, get_response_types_from_str, ResponseType
|
||||||
|
|
||||||
|
|
||||||
@oauth_bp.route("/token", methods=["POST"])
|
@oauth_bp.route("/token", methods=["POST"])
|
||||||
|
@ -62,6 +62,7 @@ def token():
|
||||||
scope=auth_code.scope,
|
scope=auth_code.scope,
|
||||||
redirect_uri=auth_code.redirect_uri,
|
redirect_uri=auth_code.redirect_uri,
|
||||||
access_token=generate_access_token(),
|
access_token=generate_access_token(),
|
||||||
|
response_type=auth_code.response_type,
|
||||||
)
|
)
|
||||||
db.session.add(oauth_token)
|
db.session.add(oauth_token)
|
||||||
|
|
||||||
|
@ -87,4 +88,10 @@ def token():
|
||||||
if oauth_token.scope and Scope.OPENID.value in oauth_token.scope:
|
if oauth_token.scope and Scope.OPENID.value in oauth_token.scope:
|
||||||
res["id_token"] = make_id_token(client_user)
|
res["id_token"] = make_id_token(client_user)
|
||||||
|
|
||||||
|
# Also return id_token if the initial flow is "code,id_token"
|
||||||
|
# cf https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
|
||||||
|
response_types = get_response_types_from_str(auth_code.response_type)
|
||||||
|
if ResponseType.ID_TOKEN in response_types:
|
||||||
|
res["id_token"] = make_id_token(client_user)
|
||||||
|
|
||||||
return jsonify(res)
|
return jsonify(res)
|
||||||
|
|
|
@ -17,6 +17,20 @@ class ResponseType(enum.Enum):
|
||||||
ID_TOKEN = "id_token"
|
ID_TOKEN = "id_token"
|
||||||
|
|
||||||
|
|
||||||
|
# All the OIDC flows supported by SimpleLogin
|
||||||
|
# CF https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
|
||||||
|
SUPPORTED_OPENID_FLOWS = [
|
||||||
|
{ResponseType.CODE},
|
||||||
|
{ResponseType.TOKEN},
|
||||||
|
{ResponseType.ID_TOKEN},
|
||||||
|
{ResponseType.ID_TOKEN, ResponseType.TOKEN},
|
||||||
|
{ResponseType.ID_TOKEN, ResponseType.CODE},
|
||||||
|
]
|
||||||
|
|
||||||
|
# String form of SUPPORTED_OPENID_FLOWS
|
||||||
|
SUPPORTED_OPENID_FLOWS_STR = "code|token|id_token|id_token,token|id_token,code"
|
||||||
|
|
||||||
|
|
||||||
def get_scopes(request: flask.Request) -> Set[Scope]:
|
def get_scopes(request: flask.Request) -> Set[Scope]:
|
||||||
scope_strs = _split_arg(request.args.getlist("scope"))
|
scope_strs = _split_arg(request.args.getlist("scope"))
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue