Skip to content

Commit 9964524

Browse files
committed
Token exchange client code.
Authorization Server metadata endpoint support.
1 parent 30e05ed commit 9964524

File tree

6 files changed

+1043
-4
lines changed

6 files changed

+1043
-4
lines changed

src/idpyoidc/client/client_auth.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,13 @@ def construct(self, request=None, service=None, http_args=None, **kwargs):
302302

303303
if service.service_name == "refresh_token":
304304
_acc_token = find_token(request, "refresh_token", service, **kwargs)
305+
elif service.service_name == "token_exchange":
306+
_acc_token = find_token(request, "subject_token", service, **kwargs)
305307
else:
306308
_acc_token = find_token(request, "access_token", service, **kwargs)
307309

308310
if not _acc_token:
309-
raise KeyError("No access or refresh token available")
311+
raise KeyError("No bearer token available")
310312

311313
# The authorization value starts with 'Bearer' when bearer tokens
312314
# are used

src/idpyoidc/client/oauth2/provider_info_discovery.py renamed to src/idpyoidc/client/oauth2/server_metadata.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
LOGGER = logging.getLogger(__name__)
1313

1414

15-
class ProviderInfoDiscovery(Service):
16-
"""The service that talks to the OAuth2 provider info discovery endpoint."""
15+
class ServerMetadata(Service):
16+
"""The service that talks to the OAuth2 server metadata endpoint."""
1717

1818
msg_type = oauth2.Message
1919
response_cls = oauth2.ASConfigurationResponse
2020
error_msg = ResponseMessage
2121
synchronous = True
22-
service_name = "provider_info"
22+
service_name = "server_metadata"
2323
http_method = "GET"
2424

2525
metadata_attributes = {}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Implements the service that can exchange one token for another."""
2+
import logging
3+
4+
from idpyoidc.client.oauth2.utils import get_state_parameter
5+
from idpyoidc.client.service import Service
6+
from idpyoidc.message import oauth2
7+
from idpyoidc.message.oauth2 import ResponseMessage
8+
from idpyoidc.time_util import time_sans_frac
9+
10+
LOGGER = logging.getLogger(__name__)
11+
12+
13+
class TokenExchange(Service):
14+
"""The token exchange service."""
15+
16+
msg_type = oauth2.TokenExchangeRequest
17+
response_cls = oauth2.TokenExchangeResponse
18+
error_msg = ResponseMessage
19+
endpoint_name = "token_endpoint"
20+
synchronous = True
21+
service_name = "token_exchange"
22+
default_authn_method = "client_secret_basic"
23+
http_method = "POST"
24+
request_body_type = "urlencoded"
25+
response_body_type = "json"
26+
27+
28+
def __init__(self, client_get, conf=None):
29+
Service.__init__(self, client_get, conf=conf)
30+
self.pre_construct.append(self.oauth_pre_construct)
31+
32+
def update_service_context(self, resp, key="", **kwargs):
33+
if "expires_in" in resp:
34+
resp["__expires_at"] = time_sans_frac() + int(resp["expires_in"])
35+
self.client_get("service_context").state.store_item(resp, "token_response", key)
36+
37+
def oauth_pre_construct(self, request_args=None, post_args=None, **kwargs):
38+
"""
39+
40+
:param request_args: Initial set of request arguments
41+
:param kwargs: Extra keyword arguments
42+
:return: Request arguments
43+
"""
44+
if request_args is None:
45+
request_args = {}
46+
47+
if 'subject_token' not in request_args:
48+
_key = get_state_parameter(request_args, kwargs)
49+
parameters = {'access_token', 'scope'}
50+
51+
_state = self.client_get("service_context").state
52+
53+
_args = _state.extend_request_args(
54+
{}, oauth2.AuthorizationResponse, "auth_response", _key, parameters
55+
)
56+
_args = _state.extend_request_args(
57+
_args, oauth2.AccessTokenResponse, "token_response", _key, parameters
58+
)
59+
_args = _state.extend_request_args(
60+
_args, oauth2.AccessTokenResponse, "refresh_token_response", _key, parameters
61+
)
62+
63+
request_args["subject_token"] = _args["access_token"]
64+
request_args["subject_token_type"] = 'urn:ietf:params:oauth:token-type:access_token'
65+
if 'scope' not in request_args and "scope" in _args:
66+
request_args["scope"] = _args["scope"]
67+
68+
return request_args, post_args
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import logging
2+
3+
from idpyoidc.message import oauth2
4+
5+
from idpyoidc.message import oidc
6+
from idpyoidc.server.endpoint import Endpoint
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class ServerMetadata(Endpoint):
12+
request_cls = oauth2.Message
13+
response_cls = oauth2.ASConfigurationResponse
14+
request_format = ""
15+
response_format = "json"
16+
name = "server_metadata"
17+
18+
def __init__(self, server_get, **kwargs):
19+
Endpoint.__init__(self, server_get=server_get, **kwargs)
20+
self.pre_construct.append(self.add_endpoints)
21+
22+
def add_endpoints(self, request, client_id, endpoint_context, **kwargs):
23+
for endpoint in [
24+
"authorization_endpoint",
25+
"registration_endpoint",
26+
"token_endpoint",
27+
"userinfo_endpoint",
28+
"end_session_endpoint",
29+
]:
30+
endp_instance = self.server_get("endpoint", endpoint)
31+
if endp_instance:
32+
request[endpoint] = endp_instance.endpoint_path
33+
34+
return request
35+
36+
def process_request(self, request=None, **kwargs):
37+
return {"response_args": self.server_get("endpoint_context").provider_info}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import os
2+
3+
from cryptojwt.key_jar import init_key_jar
4+
import pytest
5+
6+
from idpyoidc.client.entity import Entity
7+
from idpyoidc.message import Message
8+
from idpyoidc.message.oauth2 import AccessTokenResponse
9+
from idpyoidc.message.oauth2 import AuthorizationResponse
10+
from idpyoidc.message.oidc import IdToken
11+
from tests.test_client_21_oidc_service import make_keyjar
12+
13+
KEYSPEC = [
14+
{"type": "RSA", "use": ["sig"]},
15+
{"type": "EC", "crv": "P-256", "use": ["sig"]},
16+
]
17+
18+
_dirname = os.path.dirname(os.path.abspath(__file__))
19+
20+
ISS = "https://example.com"
21+
22+
ISS_KEY = init_key_jar(
23+
public_path="{}/pub_iss.jwks".format(_dirname),
24+
private_path="{}/priv_iss.jwks".format(_dirname),
25+
key_defs=KEYSPEC,
26+
issuer_id=ISS,
27+
read_only=False,
28+
)
29+
30+
ISS_KEY.import_jwks_as_json(open("{}/pub_client.jwks".format(_dirname)).read(), "client_id")
31+
32+
33+
def create_jws(val):
34+
lifetime = 3600
35+
36+
idts = IdToken(**val)
37+
38+
return idts.to_jwt(
39+
key=ISS_KEY.get_signing_key("ec", issuer_id=ISS), algorithm="ES256", lifetime=lifetime
40+
)
41+
42+
43+
class TestUserInfo(object):
44+
@pytest.fixture(autouse=True)
45+
def create_request(self):
46+
self._iss = ISS
47+
client_config = {
48+
"client_id": "client_id",
49+
"client_secret": "a longesh password",
50+
"redirect_uris": ["https://example.com/cli/authz_cb"],
51+
"issuer": self._iss,
52+
"requests_dir": "requests",
53+
"base_url": "https://example.com/cli/",
54+
}
55+
entity = Entity(keyjar=make_keyjar(), config=client_config,
56+
services={
57+
"discovery": {
58+
"class":
59+
"idpyoidc.client.oauth2.server_metadata.ServerMetadata"},
60+
"authorization": {
61+
"class": "idpyoidc.client.oauth2.authorization.Authorization"},
62+
"access_token": {
63+
"class": "idpyoidc.client.oauth2.access_token.AccessToken"},
64+
"token_exchange": {
65+
"class":
66+
"idpyoidc.client.oauth2.token_exchange.TokenExchange"
67+
},
68+
}
69+
)
70+
entity.client_get("service_context").issuer = "https://example.com"
71+
self.service = entity.client_get("service", "token_exchange")
72+
73+
_state_interface = self.service.client_get("service_context").state
74+
# Add history
75+
auth_response = AuthorizationResponse(code="access_code").to_json()
76+
_state_interface.store_item(auth_response, "auth_response", "abcde")
77+
78+
idtval = {"nonce": "KUEYfRM2VzKDaaKD", "sub": "diana", "iss": ISS, "aud": "client_id"}
79+
idt = create_jws(idtval)
80+
81+
ver_idt = IdToken().from_jwt(idt, make_keyjar())
82+
83+
token_response = AccessTokenResponse(
84+
access_token="access_token", id_token=idt, __verified_id_token=ver_idt
85+
).to_json()
86+
_state_interface.store_item(token_response, "token_response", "abcde")
87+
88+
def test_construct(self):
89+
_req = self.service.construct(state="abcde")
90+
assert isinstance(_req, Message)
91+
assert len(_req) == 2
92+
assert "subject_token" in _req

0 commit comments

Comments
 (0)