Skip to content
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

IDP token introspection #229

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open

IDP token introspection #229

wants to merge 27 commits into from

Conversation

vincent-stytch
Copy link
Contributor

@vincent-stytch vincent-stytch commented Jan 10, 2025

result = client.idp.introspect_token_network('ey....,'idp-client-test-....', 'secret')
result
IDPTokenClaims(subject='member-test-...', scopes='openid email profile', custom_claims={})

stytch/b2b/api/sessions.py Outdated Show resolved Hide resolved
stytch/b2b/api/sessions.py Outdated Show resolved Hide resolved
@vincent-stytch vincent-stytch marked this pull request as ready for review January 10, 2025 22:11
@vincent-stytch vincent-stytch requested a review from a team as a code owner January 10, 2025 22:11
@vincent-stytch vincent-stytch changed the title Idp token introspection IDP token introspection Jan 10, 2025
stytch/b2b/api/idp.py Outdated Show resolved Hide resolved
stytch/b2b/api/idp.py Outdated Show resolved Hide resolved
stytch/b2b/models/idp.py Outdated Show resolved Hide resolved
stytch/b2b/models/idp.py Outdated Show resolved Hide resolved
stytch/consumer/api/idp.py Outdated Show resolved Hide resolved
stytch/consumer/api/idp.py Outdated Show resolved Hide resolved
self.jwks_client = jwks_client
self.project_id = project_id

def introspect_idp_access_token(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need introspect_idp_access_token since unlike Session JWTs there is no case where an access token can fail local validation and pass remote validation. If the access token is expired locally, it is also guaranteed to be expired serverside.

return AccessTokenJWTClaims(
subject=jwtResponse.sub,
scope=jwtResponse.scope,
custom_claims={},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return custom claims that we retrieve from the introspection response here

token_type_hint: str = "access_token",
) -> Optional[AccessTokenJWTClaims]:
"""Introspects a token JWT from an authorization code response.
Access tokens and refresh tokens are JWTs signed with the project's JWKs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh Tokens are not JWTs - this endpoint supports both Access Token JWTs and Refresh Token opaque tokens

access_token, client_id, client_secret, token_type_hint
)

def introspect_idp_access_token_network(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nits:

  • Since the classname is already idp - do we need the second idp in the name? idp.introspect_idp_ could be idp.introspect_
  • Since this works for both access and refresh tokens, suggest removing access_ from the name

I think introspect_token_network works better.

Access tokens contain a standard set of claims as well as any custom claims generated from templates.

Fields:
- access_token: The access token (or refresh token) to introspect.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this field takes in both access tokens and refresh tokens, we should name it token

not_before=jwtResponse.nbf,
)

def introspect_idp_access_token_local(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in contrast this method is only valid for access tokens

@@ -66,6 +68,17 @@ def post(
resp = requests.post(url, json=json, headers=final_headers, auth=self.auth)
return self._response_from_request(resp)

def postForm(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we name this post_form instead? We use snake case everywhere else in this package

final_headers = self.headers.copy()
final_headers.update(headers or {})
resp = requests.post(url, data=form, headers=final_headers, auth=self.auth)
return self._response_from_request(resp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does one flavor of post_form use _response_from_post_form_request and the other not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the sync flavor uses the requests library which plays well with _response_from_request, but async's aiohttp doesn't play well here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants