-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OIDC introspection endpoint #295
base: main
Are you sure you want to change the base?
Changes from all commits
85022ea
390d230
c637efd
31170b7
933beaa
5a25408
c9fe28b
d9673ef
19c9f14
3b9a48f
f6d414b
5b15b1b
25cbab0
4eb6372
eabb0a5
ee68bf7
30f5dfa
601cd9d
5c8f733
129f82b
b73086b
e52f550
b2f2a13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,7 +31,6 @@ | |
}, | ||
|
||
"openidconnect": { | ||
"bearer_realm": "asab", | ||
"auth_code_timeout": "60 s", | ||
}, | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
import urllib | ||
import logging | ||
import aiohttp.web | ||
|
||
import asab | ||
import asab.web.rest | ||
import asab.exceptions | ||
|
||
from ...generic import nginx_introspection, get_bearer_token_value | ||
|
||
|
@@ -31,7 +31,7 @@ def __init__(self, app, oidc_svc, credentials_svc): | |
self.RBACService = app.get_service("seacatauth.RBACService") | ||
|
||
web_app = app.WebContainer.WebApp | ||
web_app.router.add_post("/openidconnect/introspect", self.introspect) | ||
web_app.router.add_post(self.OpenIdConnectService.IntrospectionPath, self.introspect) | ||
web_app.router.add_post("/nginx/introspect/openidconnect", self.introspect_nginx) | ||
|
||
# TODO: Insecure, back-compat only - remove after 2024-03-31 | ||
|
@@ -48,35 +48,92 @@ def __init__(self, app, oidc_svc, credentials_svc): | |
|
||
async def introspect(self, request): | ||
""" | ||
OAuth 2.0 Access Token Introspection Endpoint | ||
|
||
RFC7662 chapter 2 | ||
|
||
POST /introspect HTTP/1.1 | ||
Accept: application/json | ||
Content-Type: application/x-www-form-urlencoded | ||
OAuth 2.0 Token Introspection Endpoint | ||
https://datatracker.ietf.org/doc/html/rfc7662#section-2 | ||
OAuth 2.0 endpoint that takes a parameter representing an OAuth 2.0 token and returns a JSON document | ||
representing the meta information surrounding the token, including whether this token is currently active. | ||
|
||
To protect this endpoint with authorization (as required by RFC7662), use NGINX reverse proxy | ||
with auth_request to an NGINX introspection endpoint, for example: | ||
|
||
```nginx | ||
# Proxied OAuth introspection endpoint | ||
location = /openidconnect/token/introspect { | ||
auth_request /_bearer_introspect; | ||
auth_request_set $authorization $upstream_http_authorization; | ||
proxy_set_header Authorization $authorization; | ||
proxy_pass http://seacat_private_api; | ||
} | ||
|
||
token=2YotnFZFEjr1zCsicMWpAA&token_type_hint=access_token | ||
# Internal Bearer token introspection endpoint for client authentication | ||
location = /_bearer_introspect { | ||
internal; | ||
proxy_method POST; | ||
proxy_set_body "$http_authorization"; | ||
proxy_set_header X-Request-Uri "$scheme://$host$request_uri"; | ||
proxy_pass http://seacat_private_api/nginx/openidconnect; | ||
proxy_ignore_headers Cache-Control Expires Set-Cookie; | ||
} | ||
``` | ||
|
||
--- | ||
requestBody: | ||
required: true | ||
content: | ||
application/x-www-form-urlencoded: | ||
schema: | ||
type: object | ||
properties: | ||
token: | ||
type: string | ||
description: The OAuth 2.0 token to introspect. | ||
token_type_hint: | ||
type: string | ||
enum: [access_token] | ||
description: The type of token being introspected (optional). | ||
required: | ||
- token | ||
""" | ||
params = await request.post() | ||
|
||
data = await request.text() | ||
qs_data = dict(urllib.parse.parse_qsl(data)) | ||
token = params.get("token") | ||
if not token: | ||
raise asab.exceptions.ValidationError("Missing token parameter.") | ||
|
||
if qs_data.get('token_type_hint', 'access_token') != 'access_token': | ||
raise RuntimeError("Token type is not 'access_token' but '{}'".format( | ||
qs_data.get('token_type_hint', '<not provided>')) | ||
) | ||
# If the server is unable to locate the token using the given hint, it MUST extend its search across | ||
# all of its supported token types. | ||
token_type_hint = params.get("token_type_hint", "access_token") | ||
if token_type_hint != "access_token": | ||
# No other types are supported at the moment. | ||
raise asab.exceptions.ValidationError("Unsupported token_type_hint {!r}.".format(token_type_hint)) | ||
|
||
token = qs_data.get('token') | ||
if token is None: | ||
raise KeyError("Token not found") | ||
|
||
# TODO: Implement a token validation | ||
session = await self.OpenIdConnectService.get_session_by_access_token(token) | ||
if session is None: | ||
L.log(asab.LOG_NOTICE, "Access token matched no active session.") | ||
return asab.web.rest.json_response(request, {"active": False}) | ||
|
||
response = { | ||
user_info = await self.OpenIdConnectService.build_userinfo(session) | ||
response_data = { | ||
# REQUIRED | ||
"active": True, | ||
# OPTIONAL | ||
"token_type": "access_token", | ||
"client_id": user_info.get("azp"), | ||
"exp": user_info.get("exp"), | ||
"iat": user_info.get("iat"), | ||
"sub": user_info.get("sub"), | ||
"aud": user_info.get("aud"), | ||
"iss": user_info.get("iss"), | ||
} | ||
return asab.web.rest.json_response(request, response) | ||
if "preferred_username" in user_info: | ||
response_data["username"] = user_info["preferred_username"] | ||
byewokko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
elif "username" in user_info: | ||
response_data["username"] = user_info["username"] | ||
|
||
# TODO: Authorization - Verify that the requesting client is part of the token's intended audience | ||
# (i.e. that their client_id is included in the aud claim). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will break my usecase in mobile apps. Can we have a call about thiz plz? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't intend to implement this check within this merge request (or anytime soon, unless necessary). it would also require implementing a way for the client to influence the content of the i added the todo as a reminder that the authorization has some limitations and can be improved. the authorization server should not disclose id tokens to just any client. it is mostly fine in environments where there are few clients manually registered by an admin, but not in large environments with open client registration. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarified on call |
||
|
||
return asab.web.rest.json_response(request, response_data) | ||
|
||
|
||
async def _authenticate_request(self, request): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this need more documentation. So as a seacat user:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarified on call