logto
logto copied to clipboard
Scope field is empty in Token Response
Describe the bug
While authenticating user, the token response has empty scope. I would like to get the JWT access token, and as per the docs I am sending API resource param (which is the default api resource as well) without openid
scope.
If I send openid
scope (along with the resource param), then the scope property has the required scopes but the access_token is an opaque token and not a JWT.
Expected behavior
Token response should have JWT access_token and scope field filled with required scopes.
How to reproduce?
Auth Request
/oidc/auth?code_challenge=cAN9fZ-QM4TuoFCun1kpBV2Fx2Y4RpMMGfW3YzWSnB4&resource=https%3A%2F%2Fapi.mevris.app&code_challenge_method=S256&prompt=consent&redirect_uri=http%3A%2F%2Flocalhost%3A19006&client_id=gthwi145jrqgqw7itehxz&response_type=code&state=oKKHhYjXQS&scope=profile%20email%20offline_access
Auth Response
{
"type": "success",
"error": null,
"url": "http://localhost:19006/?code=UeFQ9NrME1f6L5d43HzLr9QZoZRa8wQQMnNJnqlMgj2&state=oKKHhYjXQS&iss=http%3A%2F%2Flocalhost%3A3001%2Foidc",
"params": {
"code": "UeFQ9NrME1f6L5d43HzLr9QZoZRa8wQQMnNJnqlMgj2",
"state": "oKKHhYjXQS",
"iss": "http://localhost:3001/oidc"
},
"authentication": null,
"errorCode": null
}
Token Request
grant_type=authorization_code&client_id=gthwi145jrqgqw7itehxz&scope=profile%20email%20offline_access&code_verifier=Rg81au3yQ21ZnkHlEFf7q32bZ7aAdlWsfJfMITPTmdK8sxyuonKlz5XqdbinbOrrDhKB5flnd700pXPJl3dezsgXMFUA97joeCRuNTjSSEa1YTvA3QVdci6DYdY4Qnbv&redirect_uri=http%3A%2F%2Flocalhost%3A19006&code=UeFQ9NrME1f6L5d43HzLr9QZoZRa8wQQMnNJnqlMgj2
Token Response
{
"accessToken": "eyJhbGciOiJFUzM4NCIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjFMNHVHcTQwWkV3R1VDb1M4RUp0dWZQNURaNDJXWG55UmRpTXNSMDl3eU0ifQ.eyJqdGkiOiJINXpETndkaEtVWHpGYURjbUd3OGoiLCJzdWIiOiJ4MWc4YXh2cDM0ZXgiLCJpYXQiOjE3MTM0NTAyNTEsImV4cCI6MTcxMzQ1Mzg1MSwic2NvcGUiOiIiLCJjbGllbnRfaWQiOiJndGh3aTE0NWpycWdxdzdpdGVoeHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEvb2lkYyIsImF1ZCI6Imh0dHBzOi8vYXBpLm1ldnJpcy5hcHAifQ.hGqFD8TrLY6yWDqkzIL-soVmtSFX7V7xk34lbk0Ufc1gE9WTnAFBB_Q0Z1c5fNfZINzCWPTB1YXSbcQjcE9Pjw-S0lLSCWYlxBV3mzigptmLxzNwTcsRdWFMdR-nDupa",
"tokenType": "Bearer",
"expiresIn": 3600,
"refreshToken": "rdlCL94FSp-B19Difj_wxa_0VPfvMaafpW7DGRQTuYX",
"scope": "",
"issuedAt": 1713450251
}
Did you add scopes to your resource https://api.mevris.app
, add the scopes to a user role and assign the role to your end user? After that, you also need to declare scopes you want to ask for consent when init an auth request.
openid
and email
are scopes attached to ID Token not access token.
BTW from what I can read from the context you've provided, this is not a bug, removed the "bug" tag. Feel free to bring it back with more context.
Did you add scopes to your resource
https://api.mevris.app
, add the scopes to a user role and assign the role to your end user? After that, you also need to declare scopes you want to ask for consent when init an auth request.
Do I need to do this for openid scopes as well?
openid
and
The thing is, they are add to the token in case of opaque access_token, even if ID Token is present, but when jwt is generated, the field is an empty string like the example above
Example Opaque Token
Here both resource & openid scope are sent.
Auth Request
/oidc/auth?code_challenge=DFXpT_qX8BbHx9ZFJ1g4x11XOAGpuVTDYQEnegethIs&resource=https%3A%2F%2Fapi.mevris.app&code_challenge_method=S256&prompt=consent&redirect_uri=http%3A%2F%2Flocalhost%3A19007&client_id=gthwi145jrqgqw7itehxz&response_type=code&state=q06pjx36MH&scope=openid%20profile%20email%20offline_access
Auth Response
{
"type": "success",
"error": null,
"url": "http://localhost:19007/?code=KkGRGSXph2S7ra5PzEgAdjDql2c0NLeCUszioYJ7QB-&state=q06pjx36MH&iss=http%3A%2F%2Flocalhost%3A3001%2Foidc",
"params": {
"code": "KkGRGSXph2S7ra5PzEgAdjDql2c0NLeCUszioYJ7QB-",
"state": "q06pjx36MH",
"iss": "http://localhost:3001/oidc"
},
"authentication": null,
"errorCode": null
}
Token Request
grant_type=authorization_code&client_id=gthwi145jrqgqw7itehxz&scope=openid%20profile%20email%20offline_access&code_verifier=SzzGHWVHXINPgeq5Rx96jsGwLqjlw4jcn6VhbknisE3xKIRyJxvts8ZHSkU8XuEZGB97GUh9zhjlqBz2FHZA3MIHOsqDVEqxJRmIK8PfgXxBT8IJtFxRNxb69EUWRoaK&redirect_uri=http%3A%2F%2Flocalhost%3A19007&code=KkGRGSXph2S7ra5PzEgAdjDql2c0NLeCUszioYJ7QB-
Token Response
{
"accessToken": "NZeyax74oBf8JakMdXAn-IIz1uKlUKmky8Il_0GK88P",
"tokenType": "Bearer",
"expiresIn": 3600,
"refreshToken": "fscKNjdco9-_-IfDyzsYeQkT-XptpoKRtuZNc-OzEHU",
"scope": "openid profile email offline_access",
"idToken": "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6IjFMNHVHcTQwWkV3R1VDb1M4RUp0dWZQNURaNDJXWG55UmRpTXNSMDl3eU0ifQ.eyJzdWIiOiJibm1lbzduN3p3ZWIiLCJuYW1lIjoiQWJkdWwgUmVobWFuIFRhbGF0IiwicGljdHVyZSI6Imh0dHBzOi8vbWV2cmlzLWltYWdlLWF2YXRhcnMuczMuZGUuaW8uY2xvdWQub3ZoLm5ldC83YTdhMDM0YS1kZGQ3LTQ0ZTktYTJmNy1jZmQ5MmE2NjFiNTkucG5nIiwidXNlcm5hbWUiOiJhcnRhbGF0IiwiZW1haWwiOiJyZWhtYW4udGFsYXRAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJIa1RDYk5vaFZVMGFldHBiTmlOaEVDSFJjXzlCaHVyMCIsImF1ZCI6Imd0aHdpMTQ1anJxZ3F3N2l0ZWh4eiIsImV4cCI6MTcxMzQ1NzE1NiwiaWF0IjoxNzEzNDUzNTU2LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEvb2lkYyJ9.QSMfS4nfjUH439oLx4zE_LECOOL4CJwvv8Y30O01Cm98GmvGP9x-ijaJWFXn8cidoCChIhLKTvDcgRhdZrImwxIsihve1KPijt9jhuLQ_Wk7lUu74SEWXPoWGBcINB5E",
"issuedAt": 1713453556
}
Could you please attach your SDK config? You need to add your resource and scope in the SDK config.
Im not using any official SDK, developing in expo
import * as React from 'react';
import { Button, Text, View } from 'react-native';
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
WebBrowser.maybeCompleteAuthSession();
const redirectUri = AuthSession.makeRedirectUri();
const CLIENT_ID = 'gthwi145jrqgqw7itehxz';
const SCOPES: string[] = [
'openid',
'profile',
'email',
'offline_access',
];
export default function Auth() {
const [token, setToken] = React.useState<any>();
const discovery = AuthSession.useAutoDiscovery('http://localhost:3001/oidc');
// Create and load an auth request
const [request, result, promptAsync] = AuthSession.useAuthRequest(
{
usePKCE: true,
codeChallengeMethod: AuthSession.CodeChallengeMethod.S256,
clientId: CLIENT_ID,
redirectUri,
scopes: SCOPES,
prompt: AuthSession.Prompt.Consent,
extraParams: {
resource: 'https://api.mevris.app'
}
},
discovery
);
const handleResponse = React.useCallback(
async () => {
if (result?.type !== 'success' || result.params.error) {
console.log('Something went wrong');
return;
}
const tokenResult = await AuthSession.exchangeCodeAsync(
{
scopes: SCOPES,
code: result.params.code,
clientId: CLIENT_ID,
redirectUri,
extraParams: {
code_verifier: request?.codeVerifier ? request.codeVerifier : ''
}
},
discovery as any
);
setToken(JSON.parse(JSON.stringify(tokenResult)));
},
[request, result, discovery]
);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>AuthSession Example</Text>
<Text>Redirect URI: {redirectUri}</Text>
<Text>Client ID: {CLIENT_ID}</Text>
<Text>Scopes: {SCOPES.join(', ')}</Text>
<View style={{ height: 20 }} />
<Button title="Login!" disabled={!request} onPress={() => promptAsync()} />
{result && <Text style={{width: '90%'}}>{JSON.stringify(result, null, 2)}</Text>}
<Button title="Get Token" disabled={result?.type !== 'success'} onPress={() => handleResponse()} />
{token && <Text style={{width: '90%'}}>{JSON.stringify(token, null, 2)}</Text>}
</View>
);
}
You should add your scopes to SCOPES
.
Im not using any custom scopes, intend to use only standard openid scopes for now. Problem is, event they're missing in the token response
openid
means you can get id_token
field with your token request response; email
and profile
brings corresponding information in your id_token
JWT payload (such as email
, email_verified
, name
etc.); offline_access
means refresh token comes as well in token request response. They will not show up in access token scopes but takes effect in different way like I said.
Should they also not show here (response from first example):
{
"accessToken": "eyJhbGciOiJFUzM4NCIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjFMNHVHcTQwWkV3R1VDb1M4RUp0dWZQNURaNDJXWG55UmRpTXNSMDl3eU0ifQ.eyJqdGkiOiJINXpETndkaEtVWHpGYURjbUd3OGoiLCJzdWIiOiJ4MWc4YXh2cDM0ZXgiLCJpYXQiOjE3MTM0NTAyNTEsImV4cCI6MTcxMzQ1Mzg1MSwic2NvcGUiOiIiLCJjbGllbnRfaWQiOiJndGh3aTE0NWpycWdxdzdpdGVoeHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEvb2lkYyIsImF1ZCI6Imh0dHBzOi8vYXBpLm1ldnJpcy5hcHAifQ.hGqFD8TrLY6yWDqkzIL-soVmtSFX7V7xk34lbk0Ufc1gE9WTnAFBB_Q0Z1c5fNfZINzCWPTB1YXSbcQjcE9Pjw-S0lLSCWYlxBV3mzigptmLxzNwTcsRdWFMdR-nDupa",
"tokenType": "Bearer",
"expiresIn": 3600,
"refreshToken": "rdlCL94FSp-B19Difj_wxa_0VPfvMaafpW7DGRQTuYX",
"scope": "",
"issuedAt": 1713450251
}
Notice "scope": ""
Also, no ID Token issued here.
id_token
will not appear when you call token endpoint with refresh_token
grant type. If you do not have custom scope and do not specify when init auth requests, what do you expect from access token scopes?
token request with authorization_code
grant type and the code
brought to callback URL with openid
in the previous auth request ensure the appearance of id_token
in the token response.
Im expecting, "scope": "openid profile email offline_access"
.
Please guide, when I have the following response, which I am getting in my use case, how do I check scopes. Moreover in the JWT there are no profile or email claims.
{
"accessToken": "eyJhbGciOiJFUzM4NCIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjFMNHVHcTQwWkV3R1VDb1M4RUp0dWZQNURaNDJXWG55UmRpTXNSMDl3eU0ifQ.eyJqdGkiOiJINXpETndkaEtVWHpGYURjbUd3OGoiLCJzdWIiOiJ4MWc4YXh2cDM0ZXgiLCJpYXQiOjE3MTM0NTAyNTEsImV4cCI6MTcxMzQ1Mzg1MSwic2NvcGUiOiIiLCJjbGllbnRfaWQiOiJndGh3aTE0NWpycWdxdzdpdGVoeHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEvb2lkYyIsImF1ZCI6Imh0dHBzOi8vYXBpLm1ldnJpcy5hcHAifQ.hGqFD8TrLY6yWDqkzIL-soVmtSFX7V7xk34lbk0Ufc1gE9WTnAFBB_Q0Z1c5fNfZINzCWPTB1YXSbcQjcE9Pjw-S0lLSCWYlxBV3mzigptmLxzNwTcsRdWFMdR-nDupa",
"tokenType": "Bearer",
"expiresIn": 3600,
"refreshToken": "rdlCL94FSp-B19Difj_wxa_0VPfvMaafpW7DGRQTuYX",
"scope": "",
"issuedAt": 1713450251
}
My requirement is very simple, I want to generate a JWT token (not an opaque token), that has the required claims (profile, email, etc). Our system was desinged on keycloak, and we had claims in the access token. We are trying to replicate the same here, to migrate to Logto
I think you're mixing up the concepts of OIDC scopes and API resource scopes.
The scopes you mentioned (openid, profile, offline_access, etc.) are OIDC scopes, which are primarily focused on user authentication and providing identity-related information. The consumer of these scopes is the OIDC auth server, not your own API server.
API resource scopes are focused on controlling access to specific functionalities or data within an API, and this is what you really want in your access tokens. You can only define these scopes in an "API resource". Simply go to "Logto console -> API resources" and create an API resource first, then create your scopes under the context of your API resource. Your backend service should check these scopes rather than the OIDC scopes.
Moreover, The "opaque" token you obtained earlier is the one that used to fetch user information from the /userinfo
endpoint. You can not use this access token for your own API services. Please refer to the documentations and learn how to protect your own APIs with API resources. https://docs.logto.io/docs/recipes/protect-your-api/
My requirement is very simple, I want to generate a JWT token (not an opaque token), that has the required claims (profile, email, etc). Our system was desinged on keycloak, and we had claims in the access token. We are trying to replicate the same here, to migrate to Logto
Anyway I can achieve this?
Why do you need these claims in a JWT token when fetching userinfo, though? These information is returned from the /userinfo
endpoind, which is hosted by Logto itself, not a third party. An opaque token is enough in this case since Logto can always check the DB if the received opaque token is valid. And the scopes you mentioned are pre-configured through LogtoConfigs
in SDK.
JWT is for "offline verification" purpose, usually used when requesting from client to server, or a 3rd-party API service. In these scenarios, you'll have to provide a JWT format token, so that the receiver can check if it is valid and you have proper scopes for the requesting resource even if the token is not issued by themselves.
I'm not sure what SDK you are using, but let me explain it with the React SDK.
You need to provide these scopes through LogtoConfigs
and pass it to Logto client
Then whenever you call fetchUserInfo
or decode ID token claims, you will find you always get "tailored" userinfo based on the predefined scopes
in LogtoConfig.
This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.