Add groups claim and scope

This commit is contained in:
Camille Daniel 2021-06-03 17:24:36 +02:00
parent 294b86a698
commit f1ac9e140a
14 changed files with 90 additions and 20 deletions

View file

@ -18,7 +18,7 @@
"https://mydomain.tld/oauth/register",
"scopes_supported":
["openid", "profile", "email", "address",
"phone"],
"phone", "groups"],
"response_types_supported":
["code", "token", "id_token", "code token",
"code id_token", "token id_token"],

View file

@ -22,7 +22,7 @@
"https://mydomain.tld/oauth/register",
"scopes_supported":
["openid", "profile", "email", "address",
"phone"],
"phone", "groups"],
"response_types_supported":
["code", "token", "id_token", "code token",
"code id_token", "token id_token"],

View file

@ -27,6 +27,7 @@ CLAIMS = {
"email": ("at", _("Your email address.")),
"address": ("envelope open outline", _("Your postal address.")),
"phone": ("phone", _("Your phone number.")),
"groups": ("users", _("Groups you are belonging to")),
}

View file

@ -61,6 +61,8 @@ def generate_user_info(user, scope):
fields += ["address"]
if "phone" in scope:
fields += ["phone_number", "phone_number_verified"]
if "groups" in scope:
fields += ["groups"]
data = {}
for field in fields:
@ -69,6 +71,9 @@ def generate_user_info(user, scope):
data[field] = user.__getattr__(ldap_field_match)
if isinstance(data[field], list):
data[field] = data[field][0]
if field == "groups":
group_name_attr = current_app.config["LDAP"]["GROUP_NAME_ATTRIBUTE"]
data[field] = [getattr(g, group_name_attr)[0] for g in user.groups]
return UserInfo(**data)

View file

@ -17,7 +17,7 @@ def create_app():
server_metadata_url=get_well_known_url(
app.config["OAUTH_AUTH_SERVER"], external=True
),
client_kwargs={"scope": "openid profile email"},
client_kwargs={"scope": "openid profile email groups"},
)
@app.route("/")

View file

@ -53,7 +53,10 @@
<h2 class="ui header">{{ name }}</h2>
<div>
{% if user %}
Welcome {{ user.name }}
<p>Welcome {{ user.name }}</p>
{% if user.groups %}
<p>You're a member of the following groups: {{ user.groups }}</p>
{% endif %}
{% else %}
Welcome, please <a href="{{ url_for('login') }}">log-in</a>.
{% endif %}

View file

@ -18,7 +18,7 @@
"http://localhost:5000/oauth/register",
"scopes_supported":
["openid", "profile", "email", "address",
"phone"],
"phone", "groups"],
"response_types_supported":
["code", "token", "id_token", "code token",
"code id_token", "token id_token"],

View file

@ -22,7 +22,7 @@
"http://localhost:5000/oauth/register",
"scopes_supported":
["openid", "profile", "email", "address",
"phone"],
"phone", "groups"],
"response_types_supported":
["code", "token", "id_token", "code token",
"code id_token", "token id_token"],

View file

@ -94,6 +94,7 @@ oauthGrantType: refresh_token
oauthScope: openid
oauthScope: profile
oauthScope: email
oauthScope: groups
oauthResponseType: code
oauthResponseType: id_token
oauthTokenEndpointAuthMethod: client_secret_basic
@ -111,6 +112,7 @@ oauthGrantType: refresh_token
oauthScope: openid
oauthScope: profile
oauthScope: email
oauthScope: groups
oauthResponseType: code
oauthResponseType: id_token
oauthTokenEndpointAuthMethod: client_secret_basic

View file

@ -215,7 +215,7 @@ def client(app, slapd_connection):
"refresh_token",
],
oauthResponseType=["code", "token", "id_token"],
oauthScope=["openid", "profile"],
oauthScope=["openid", "profile", "groups"],
oauthTermsOfServiceURI="https://mydomain.tld/tos",
oauthPolicyURI="https://mydomain.tld/policy",
oauthJWKURI="https://mydomain.tld/jwk",
@ -361,7 +361,10 @@ def foo_group(app, user, slapd_connection):
g.save(slapd_connection)
with app.app_context():
user.load_groups(conn=slapd_connection)
return g
yield g
user._groups = []
g.delete(conn=slapd_connection)
@pytest.fixture
@ -375,7 +378,9 @@ def bar_group(app, admin, slapd_connection):
g.save(slapd_connection)
with app.app_context():
admin.load_groups(conn=slapd_connection)
return g
yield g
admin._groups = []
g.delete(conn=slapd_connection)
@pytest.fixture

View file

@ -47,7 +47,7 @@ def test_authorization_code_flow(testclient, slapd_connection, logged_user, clie
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json
assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json
def test_logout_login(testclient, slapd_connection, logged_user, client):
@ -105,7 +105,7 @@ def test_logout_login(testclient, slapd_connection, logged_user, client):
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json
assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json
def test_refresh_token(testclient, slapd_connection, logged_user, client):
@ -164,7 +164,7 @@ def test_refresh_token(testclient, slapd_connection, logged_user, client):
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json
assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json
def test_code_challenge(testclient, slapd_connection, logged_user, client):
@ -218,7 +218,7 @@ def test_code_challenge(testclient, slapd_connection, logged_user, client):
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json
assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json
client.oauthTokenEndpointAuthMethod = "client_secret_basic"
client.save(slapd_connection)

View file

@ -1,9 +1,10 @@
from authlib.jose import jwt
from urllib.parse import urlsplit, parse_qs
from canaille.models import AuthorizationCode, Token
from canaille.models import AuthorizationCode, Token, User
def test_oauth_hybrid(testclient, slapd_connection, user, client):
User.attr_type_by_name(slapd_connection)
res = testclient.get(
"/oauth/authorize",
params=dict(
@ -41,7 +42,7 @@ def test_oauth_hybrid(testclient, slapd_connection, user, client):
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json
assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json
def test_oidc_hybrid(testclient, slapd_connection, logged_user, client, keypair):
@ -80,4 +81,4 @@ def test_oidc_hybrid(testclient, slapd_connection, logged_user, client, keypair)
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert {"name": "John Doe", "family_name": "Doe", "sub": "user"} == res.json
assert {"name": "John Doe", "family_name": "Doe", "sub": "user", "groups": []} == res.json

View file

@ -40,7 +40,7 @@ def test_oauth_implicit(testclient, slapd_connection, user, client):
"/oauth/userinfo", headers={"Authorization": f"Bearer {access_token}"}
)
assert "application/json" == res.content_type
assert {"name": "John Doe", "sub": "user", "family_name": "Doe"} == res.json
assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": []} == res.json
client.oauthGrantType = ["code"]
client.oauthTokenEndpointAuthMethod = "client_secret_basic"
@ -92,7 +92,60 @@ def test_oidc_implicit(testclient, keypair, slapd_connection, user, client):
status=200,
)
assert "application/json" == res.content_type
assert {"name": "John Doe", "sub": "user", "family_name": "Doe"} == res.json
assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": []} == res.json
client.oauthGrantType = ["code"]
client.oauthTokenEndpointAuthMethod = "client_secret_basic"
client.save(slapd_connection)
def test_oidc_implicit_with_group(testclient, keypair, slapd_connection, user, client, foo_group):
client.oauthGrantType = ["token id_token"]
client.oauthTokenEndpointAuthMethod = "none"
client.save(slapd_connection)
res = testclient.get(
"/oauth/authorize",
params=dict(
response_type="id_token token",
client_id=client.oauthClientID,
scope="openid profile groups",
nonce="somenonce",
),
)
assert "text/html" == res.content_type
res.form["login"] = "user"
res.form["password"] = "correct horse battery staple"
res = res.form.submit(status=302)
res = res.follow(status=200)
assert "text/html" == res.content_type, res.json
res = res.form.submit(name="answer", value="accept", status=302)
assert res.location.startswith(client.oauthRedirectURIs[0])
params = parse_qs(urlsplit(res.location).fragment)
access_token = params["access_token"][0]
token = Token.get(access_token, conn=slapd_connection)
assert token is not None
id_token = params["id_token"][0]
claims = jwt.decode(id_token, keypair[1])
assert user.uid[0] == claims["sub"]
assert user.cn[0] == claims["name"]
assert [client.oauthClientID] == claims["aud"]
assert ["foo"] == claims["groups"]
res = testclient.get(
"/oauth/userinfo",
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert "application/json" == res.content_type
assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": ["foo"]} == res.json
client.oauthGrantType = ["code"]
client.oauthTokenEndpointAuthMethod = "client_secret_basic"

View file

@ -15,7 +15,7 @@ def test_password_flow(testclient, slapd_connection, user, client):
status=200,
)
assert res.json["scope"] == "openid profile"
assert res.json["scope"] == "openid profile groups"
assert res.json["token_type"] == "Bearer"
access_token = res.json["access_token"]
@ -27,4 +27,4 @@ def test_password_flow(testclient, slapd_connection, user, client):
headers={"Authorization": f"Bearer {access_token}"},
status=200,
)
assert {"name": "John Doe", "sub": "user", "family_name": "Doe"} == res.json
assert {"name": "John Doe", "sub": "user", "family_name": "Doe", "groups": []} == res.json