forked from Github-Mirrors/canaille
Add groups claim and scope
This commit is contained in:
parent
294b86a698
commit
f1ac9e140a
14 changed files with 90 additions and 20 deletions
|
@ -18,7 +18,7 @@
|
||||||
"https://mydomain.tld/oauth/register",
|
"https://mydomain.tld/oauth/register",
|
||||||
"scopes_supported":
|
"scopes_supported":
|
||||||
["openid", "profile", "email", "address",
|
["openid", "profile", "email", "address",
|
||||||
"phone"],
|
"phone", "groups"],
|
||||||
"response_types_supported":
|
"response_types_supported":
|
||||||
["code", "token", "id_token", "code token",
|
["code", "token", "id_token", "code token",
|
||||||
"code id_token", "token id_token"],
|
"code id_token", "token id_token"],
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
"https://mydomain.tld/oauth/register",
|
"https://mydomain.tld/oauth/register",
|
||||||
"scopes_supported":
|
"scopes_supported":
|
||||||
["openid", "profile", "email", "address",
|
["openid", "profile", "email", "address",
|
||||||
"phone"],
|
"phone", "groups"],
|
||||||
"response_types_supported":
|
"response_types_supported":
|
||||||
["code", "token", "id_token", "code token",
|
["code", "token", "id_token", "code token",
|
||||||
"code id_token", "token id_token"],
|
"code id_token", "token id_token"],
|
||||||
|
|
|
@ -27,6 +27,7 @@ CLAIMS = {
|
||||||
"email": ("at", _("Your email address.")),
|
"email": ("at", _("Your email address.")),
|
||||||
"address": ("envelope open outline", _("Your postal address.")),
|
"address": ("envelope open outline", _("Your postal address.")),
|
||||||
"phone": ("phone", _("Your phone number.")),
|
"phone": ("phone", _("Your phone number.")),
|
||||||
|
"groups": ("users", _("Groups you are belonging to")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,8 @@ def generate_user_info(user, scope):
|
||||||
fields += ["address"]
|
fields += ["address"]
|
||||||
if "phone" in scope:
|
if "phone" in scope:
|
||||||
fields += ["phone_number", "phone_number_verified"]
|
fields += ["phone_number", "phone_number_verified"]
|
||||||
|
if "groups" in scope:
|
||||||
|
fields += ["groups"]
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
for field in fields:
|
for field in fields:
|
||||||
|
@ -69,6 +71,9 @@ def generate_user_info(user, scope):
|
||||||
data[field] = user.__getattr__(ldap_field_match)
|
data[field] = user.__getattr__(ldap_field_match)
|
||||||
if isinstance(data[field], list):
|
if isinstance(data[field], list):
|
||||||
data[field] = data[field][0]
|
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)
|
return UserInfo(**data)
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ def create_app():
|
||||||
server_metadata_url=get_well_known_url(
|
server_metadata_url=get_well_known_url(
|
||||||
app.config["OAUTH_AUTH_SERVER"], external=True
|
app.config["OAUTH_AUTH_SERVER"], external=True
|
||||||
),
|
),
|
||||||
client_kwargs={"scope": "openid profile email"},
|
client_kwargs={"scope": "openid profile email groups"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
|
@ -53,7 +53,10 @@
|
||||||
<h2 class="ui header">{{ name }}</h2>
|
<h2 class="ui header">{{ name }}</h2>
|
||||||
<div>
|
<div>
|
||||||
{% if user %}
|
{% 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 %}
|
{% else %}
|
||||||
Welcome, please <a href="{{ url_for('login') }}">log-in</a>.
|
Welcome, please <a href="{{ url_for('login') }}">log-in</a>.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
"http://localhost:5000/oauth/register",
|
"http://localhost:5000/oauth/register",
|
||||||
"scopes_supported":
|
"scopes_supported":
|
||||||
["openid", "profile", "email", "address",
|
["openid", "profile", "email", "address",
|
||||||
"phone"],
|
"phone", "groups"],
|
||||||
"response_types_supported":
|
"response_types_supported":
|
||||||
["code", "token", "id_token", "code token",
|
["code", "token", "id_token", "code token",
|
||||||
"code id_token", "token id_token"],
|
"code id_token", "token id_token"],
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
"http://localhost:5000/oauth/register",
|
"http://localhost:5000/oauth/register",
|
||||||
"scopes_supported":
|
"scopes_supported":
|
||||||
["openid", "profile", "email", "address",
|
["openid", "profile", "email", "address",
|
||||||
"phone"],
|
"phone", "groups"],
|
||||||
"response_types_supported":
|
"response_types_supported":
|
||||||
["code", "token", "id_token", "code token",
|
["code", "token", "id_token", "code token",
|
||||||
"code id_token", "token id_token"],
|
"code id_token", "token id_token"],
|
||||||
|
|
|
@ -94,6 +94,7 @@ oauthGrantType: refresh_token
|
||||||
oauthScope: openid
|
oauthScope: openid
|
||||||
oauthScope: profile
|
oauthScope: profile
|
||||||
oauthScope: email
|
oauthScope: email
|
||||||
|
oauthScope: groups
|
||||||
oauthResponseType: code
|
oauthResponseType: code
|
||||||
oauthResponseType: id_token
|
oauthResponseType: id_token
|
||||||
oauthTokenEndpointAuthMethod: client_secret_basic
|
oauthTokenEndpointAuthMethod: client_secret_basic
|
||||||
|
@ -111,6 +112,7 @@ oauthGrantType: refresh_token
|
||||||
oauthScope: openid
|
oauthScope: openid
|
||||||
oauthScope: profile
|
oauthScope: profile
|
||||||
oauthScope: email
|
oauthScope: email
|
||||||
|
oauthScope: groups
|
||||||
oauthResponseType: code
|
oauthResponseType: code
|
||||||
oauthResponseType: id_token
|
oauthResponseType: id_token
|
||||||
oauthTokenEndpointAuthMethod: client_secret_basic
|
oauthTokenEndpointAuthMethod: client_secret_basic
|
||||||
|
|
|
@ -215,7 +215,7 @@ def client(app, slapd_connection):
|
||||||
"refresh_token",
|
"refresh_token",
|
||||||
],
|
],
|
||||||
oauthResponseType=["code", "token", "id_token"],
|
oauthResponseType=["code", "token", "id_token"],
|
||||||
oauthScope=["openid", "profile"],
|
oauthScope=["openid", "profile", "groups"],
|
||||||
oauthTermsOfServiceURI="https://mydomain.tld/tos",
|
oauthTermsOfServiceURI="https://mydomain.tld/tos",
|
||||||
oauthPolicyURI="https://mydomain.tld/policy",
|
oauthPolicyURI="https://mydomain.tld/policy",
|
||||||
oauthJWKURI="https://mydomain.tld/jwk",
|
oauthJWKURI="https://mydomain.tld/jwk",
|
||||||
|
@ -361,7 +361,10 @@ def foo_group(app, user, slapd_connection):
|
||||||
g.save(slapd_connection)
|
g.save(slapd_connection)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
user.load_groups(conn=slapd_connection)
|
user.load_groups(conn=slapd_connection)
|
||||||
return g
|
yield g
|
||||||
|
user._groups = []
|
||||||
|
g.delete(conn=slapd_connection)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -375,7 +378,9 @@ def bar_group(app, admin, slapd_connection):
|
||||||
g.save(slapd_connection)
|
g.save(slapd_connection)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
admin.load_groups(conn=slapd_connection)
|
admin.load_groups(conn=slapd_connection)
|
||||||
return g
|
yield g
|
||||||
|
admin._groups = []
|
||||||
|
g.delete(conn=slapd_connection)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
@ -47,7 +47,7 @@ def test_authorization_code_flow(testclient, slapd_connection, logged_user, clie
|
||||||
headers={"Authorization": f"Bearer {access_token}"},
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
status=200,
|
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):
|
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}"},
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
status=200,
|
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):
|
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}"},
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
status=200,
|
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):
|
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}"},
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
status=200,
|
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.oauthTokenEndpointAuthMethod = "client_secret_basic"
|
||||||
client.save(slapd_connection)
|
client.save(slapd_connection)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from authlib.jose import jwt
|
from authlib.jose import jwt
|
||||||
from urllib.parse import urlsplit, parse_qs
|
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):
|
def test_oauth_hybrid(testclient, slapd_connection, user, client):
|
||||||
|
User.attr_type_by_name(slapd_connection)
|
||||||
res = testclient.get(
|
res = testclient.get(
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
params=dict(
|
params=dict(
|
||||||
|
@ -41,7 +42,7 @@ def test_oauth_hybrid(testclient, slapd_connection, user, client):
|
||||||
headers={"Authorization": f"Bearer {access_token}"},
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
status=200,
|
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):
|
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}"},
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
status=200,
|
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
|
||||||
|
|
|
@ -40,7 +40,7 @@ def test_oauth_implicit(testclient, slapd_connection, user, client):
|
||||||
"/oauth/userinfo", headers={"Authorization": f"Bearer {access_token}"}
|
"/oauth/userinfo", headers={"Authorization": f"Bearer {access_token}"}
|
||||||
)
|
)
|
||||||
assert "application/json" == res.content_type
|
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.oauthGrantType = ["code"]
|
||||||
client.oauthTokenEndpointAuthMethod = "client_secret_basic"
|
client.oauthTokenEndpointAuthMethod = "client_secret_basic"
|
||||||
|
@ -92,7 +92,60 @@ def test_oidc_implicit(testclient, keypair, slapd_connection, user, client):
|
||||||
status=200,
|
status=200,
|
||||||
)
|
)
|
||||||
assert "application/json" == res.content_type
|
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.oauthGrantType = ["code"]
|
||||||
client.oauthTokenEndpointAuthMethod = "client_secret_basic"
|
client.oauthTokenEndpointAuthMethod = "client_secret_basic"
|
||||||
|
|
|
@ -15,7 +15,7 @@ def test_password_flow(testclient, slapd_connection, user, client):
|
||||||
status=200,
|
status=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res.json["scope"] == "openid profile"
|
assert res.json["scope"] == "openid profile groups"
|
||||||
assert res.json["token_type"] == "Bearer"
|
assert res.json["token_type"] == "Bearer"
|
||||||
access_token = res.json["access_token"]
|
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}"},
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
status=200,
|
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
|
||||||
|
|
Loading…
Reference in a new issue