Get a bearer token with user roles via the /oauth/token API

My standard login process via the /authorize url is working fine and I have set it up to include user roles in the returned token with the following action:

exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://blah.blah.com';  // Use a custom namespace
  // Check if event.authorization and roles exist
  const assignedRoles = event.authorization && event.authorization.roles 
    ? event.authorization.roles 
    : [];

  // Set roles only if they exist
  if (assignedRoles.length > 0) {
    api.idToken.setCustomClaim(`${namespace}/roles`, assignedRoles);
    api.accessToken.setCustomClaim(`${namespace}/roles`, assignedRoles);
  }
};

Now for testing I want to be able to get a token for specific user types without requiring user input. To do this I believe the correct way is to use the /oauth/token endpoint. I’ve tried this with the “client_credentials” grant type:

async fn get_auth_token_cc() -> String {
    let client = reqwest::Client::new();
    let domain = std::env::var("DOMAIN").expect("DOMAIN not set");
    let res = client
        .post(&format!("{}/oauth/token", domain))
        .header("content-type", "application/json")
        .json(&serde_json::json!({
            "client_id": std::env::var("CLIENT_ID").expect("CLIENT_ID not set"),
            "client_secret": std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"),
            "audience": &format!("{}/api/v2/", domain),
            "grant_type": "client_credentials",
            "username": std::env::var("AUTH0_USER_UN").expect("AUTH0_USER_UN not set"),
            "password": std::env::var("AUTH0_USER_PW").expect("AUTH0_USER_PW not set"),
        }))
        .send()
        .await
        .expect("Failed to get token");

    info!("{:?}", res);
    let token: Auth0TokenResponse = res.json().await.expect("Invalid token response");
    token.access_token
}

This succeeds but provides a token without any user or user role information.

I then thought that I needed to create a custom API for my Application and do an endpoint request with the “password” grant type:


async fn get_auth_token() -> String {
    let client = reqwest::Client::new();
    let domain = std::env::var("DOMAIN").expect("DOMAIN not set");
    let res = client
        .post(&format!("{}/oauth/token", domain))
        .header("content-type", "application/json")
        .json(&serde_json::json!({
            "grant_type": "password",
            "client_id": std::env::var("CLIENT_ID").expect("CLIENT_ID not set"),
            "client_secret": std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"),
            "audience": std::env::var("AUTH0_AUDIENCE").expect("AUTH0_AUDIENCE not set"),
            "username": std::env::var("AUTH0_USER_UN").expect("AUTH0_USER_UN not set"),
            "password": std::env::var("AUTH0_USER_PW").expect("AUTH0_USER_PW not set"),
            "scope": "openid profile email",
            "realm": "Username-Password-Authentication"
        }))
        .send()
        .await
        .expect("Failed to get token");

    info!("{:?}", res);
    let token: Auth0TokenResponse = res.json().await.expect("Invalid token response");
    token.access_token
}

However this returns a 500 “Authorization server not configured with default connection”. Here is the full response:

{
   "url":"https://dev-gdmpjq3p28gmc875.uk.auth0.com/oauth/token",
   "status":500,
   "headers":{
      "date":"Mon, 07 Apr 2025 20:04:44 GMT",
      "content-type":"application/json",
      "content-length":"107",
      "connection":"keep-alive",
      "cf-ray":"92cc22beddcd6519-LHR",
      "cf-cache-status":"DYNAMIC",
      "cache-control":"private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform",
      "set-cookie":"did=s%3Av0%3A3c8852bf-20e3-4269-9159-26ad7f477809.L7xzF7%2FmbWo7pKdPtNXibEcWkb1DrVet6cbR4dpK84k; Path=/; Expires=Wed, 08 Apr 2026 02:04:44 GMT; HttpOnly; Secure; SameSite=None",
      "set-cookie":"did_compat=s%3Av0%3A3c8852bf-20e3-4269-9159-26ad7f477809.L7xzF7%2FmbWo7pKdPtNXibEcWkb1DrVet6cbR4dpK84k; Path=/; Expires=Wed, 08 Apr 2026 02:04:44 GMT; HttpOnly; Secure",
      "strict-transport-security":"max-age=31536000; includeSubDomains",
      "vary":"Origin",
      "x-auth0-l":"0.033",
      "x-auth0-requestid":"09abe69cf770170cd8ed",
      "x-content-type-options":"nosniff",
      "x-ratelimit-limit":"300",
      "x-ratelimit-remaining":"299",
      "x-ratelimit-reset":"1744056345",
      "server":"cloudflare",
      "alt-svc":"h3=\":443\"; ma=86400"
   }
}

Do I have the correct method? If so, is there a setting I need to configure on the dashboard to get this to work?

Thanks!

Hi @pdaj-atl

Welcome to the Auth0 Community!

Once you have assigned roles to the user and they are visible on the Dashboard under User Managemenr → Users → Select User → Roles they should be visible in the ID Token after they have been set as custom claims using your action.

I have tested this using Postman and after decoding the JWT on jwt.io this is the result:

{
  "https://blah.blah.com/roles": [
    "Secondary Test",
    "Test"
  ],
  "email": "{test_email}}",
  "email_verified": false,
  "nickname": "{{nickname}}",
  "name": "{{name}}",
  "picture": "{{picture_url}}",
  "updated_at": "2025-04-08T18:14:34.260Z",
  "iss": "{{auth0_domain}}",
  "aud": "{{audience}}",
  "sub": "{{user_id}}",
  "iat": 1744136075,
  "exp": 1744172075
}

I have tested the endpoint using Postman and this is the configuration I have used:

The only differences that I can spot is that you are using the grant_type of password not password-realm, even though you are declaring the realm in your body, and that you are using application/json whereas I am using the urlencoded since I am using postman.

Could you please try switching topassword-realm for the grant type? Make sure to enable the grant for Password inside the application that you are using.

Let me know if this was helpful or if you have any other questions!

Kind Regards,
Nik

Hi Nik,
Thanks for your response. I’m still having problems. I’ve tried to recreate your exact call:

async fn get_auth_token_pw() -> String {
    let client = reqwest::Client::new();
    let domain = std::env::var("DOMAIN").expect("DOMAIN not set");
    let res = client
        .post(&format!("{}/oauth/token", domain))
        .header("content-type", "application/json")
        .json(&serde_json::json!({
            "grant_type": "password-realm",
            "client_id": std::env::var("CLIENT_ID").expect("CLIENT_ID not set"),
            "client_secret": std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"),
            "audience": &format!("{}/api/v2/", domain),
            "username": std::env::var("AUTH0_USER_UN").expect("AUTH0_USER_UN not set"),
            "password": std::env::var("AUTH0_USER_PW").expect("AUTH0_USER_PW not set"),
            "realm": "Username-Password-Authentication",
            "scope": "openid email profile read:current_user offline_access",
        }))
        .send()
        .await
        .expect("Failed to get token");

    info!("{:#?}", res);

    if res.status().is_success() {
        let token: Auth0TokenResponse = res.json().await.expect("Invalid token response");
        info!("{:#?}", token);
        token.access_token
    } else {
        "NO TOKEN".to_owned()
    }
}

And using this gives:

Response {
    url: "https://<domain>/oauth/token",
    status: 403,
    headers: {
        "date": "Wed, 09 Apr 2025 09:16:55 GMT",
        "content-type": "application/json",
        "content-length": "173",
        "connection": "keep-alive",
        "cf-ray": "92d8e88cbb5aa690-LHR",
        "cf-cache-status": "DYNAMIC",
        "cache-control": "private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform",
        "set-cookie": "did=s%3Av0%3Afa6055a6-079f-4cc9-9695-7e8a70b20455.aX2yPsBAXnsL50WcvSQDg3xz3kdgLdouuKMVkMKDRDM; Path=/; Expires=Thu, 09 Apr 2026 15:16:55 GMT; HttpOnly; Secure; SameSite=None",
        "set-cookie": "did_compat=s%3Av0%3Afa6055a6-079f-4cc9-9695-7e8a70b20455.aX2yPsBAXnsL50WcvSQDg3xz3kdgLdouuKMVkMKDRDM; Path=/; Expires=Thu, 09 Apr 2026 15:16:55 GMT; HttpOnly; Secure",
        "strict-transport-security": "max-age=31536000; includeSubDomains",
        "vary": "Origin",
        "x-auth0-l": "0.030",
        "x-auth0-requestid": "5e90fd85224c912a9366",
        "x-content-type-options": "nosniff",
        "x-ratelimit-limit": "300",
        "x-ratelimit-remaining": "299",
        "x-ratelimit-reset": "1744190276",
        "server": "cloudflare",
        "alt-svc": "h3=\":443\"; ma=86400",
    },
}

Using the “client_credentials” grant type with this code:

async fn get_auth_token_cc() -> String {
    let client = reqwest::Client::new();
    let domain = std::env::var("DOMAIN").expect("DOMAIN not set");
    let res = client
        .post(&format!("{}/oauth/token", domain))
        .header("content-type", "application/json")
        .json(&serde_json::json!({
            "grant_type": "client_credentials",
            "client_id": std::env::var("CLIENT_ID").expect("CLIENT_ID not set"),
            "client_secret": std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"),
            "audience": &format!("{}/api/v2/", domain),
            "username": std::env::var("AUTH0_USER_UN").expect("AUTH0_USER_UN not set"),
            "password": std::env::var("AUTH0_USER_PW").expect("AUTH0_USER_PW not set"),
        }))
        .send()
        .await
        .expect("Failed to get token");

    info!("{:#?}", res);

    if res.status().is_success() {
        let token: Auth0TokenResponse = res.json().await.expect("Invalid token response");
        info!("{:#?}", token);
        token.access_token
    } else {
        "NO TOKEN".to_owned()
    }
}

This “works” in the sense that the endpoint succeeds:

Response {
    url: "https://<domain>/oauth/token",
    status: 200,
    headers: {
        "date": "Wed, 09 Apr 2025 09:16:55 GMT",
        "content-type": "application/json",
        "transfer-encoding": "chunked",
        "connection": "keep-alive",
        "cf-ray": "92d8e88dc96271c6-LHR",
        "cf-cache-status": "DYNAMIC",
        "cache-control": "no-store",
        "set-cookie": "did=s%3Av0%3A85601f5b-5e95-453a-8a2b-129aff7f650f.UCChErkp%2Fwd2pf761ZCLbujoVxYzBlqIs2dbEjYlO8E; Path=/; Expires=Thu, 09 Apr 2026 15:16:55 GMT; HttpOnly; Secure; SameSite=None",
        "set-cookie": "did_compat=s%3Av0%3A85601f5b-5e95-453a-8a2b-129aff7f650f.UCChErkp%2Fwd2pf761ZCLbujoVxYzBlqIs2dbEjYlO8E; Path=/; Expires=Thu, 09 Apr 2026 15:16:55 GMT; HttpOnly; Secure",
        "strict-transport-security": "max-age=31536000; includeSubDomains",
        "vary": "Accept-Encoding, Origin",
        "pragma": "no-cache",
        "x-auth0-dl": "21",
        "x-auth0-l": "0.088",
        "x-auth0-requestid": "1fb853675396699dab70",
        "x-content-type-options": "nosniff",
        "x-ratelimit-limit": "300",
        "x-ratelimit-remaining": "298",
        "x-ratelimit-reset": "1744190276",
        "server": "cloudflare",
        "alt-svc": "h3=\":443\"; ma=86400",
    },
}

But the user information is not in the decoded JWT:

Claims {
    sub: "FFerjBdGic0fN5zC7bHqPHfL65anuwBY@clients",
    exp: 1744276615,
    email: None,
    name: None,
    roles: None,
}

My conclusion is that “client_credentials” grant type can’t provide a token with the user information and “password-realm” grant type is for some reason blocked for me. I have looked through the dashboard settings but can’t see anything obvious. My application does have the “Password” grant type ticked in Advanced settings.

Hi again,

Indeed, the client_credentials grant will not return an ID Token.
However, could you please try to use http://auth0.com/oauth/grant-type/password-realm instead of just password-realm.

Alternatively, please try the following:

  • Navigate to your dashboard
  • On the left menu, click on Setting
  • Scroll down to “API Authorization Settings”
  • Enter Username-Password-Authentication in the “Default Directory” input
  • Hit save

After you have done this, try running your initial code:

async fn get_auth_token() -> String {
    let client = reqwest::Client::new();
    let domain = std::env::var("DOMAIN").expect("DOMAIN not set");
    let res = client
        .post(&format!("{}/oauth/token", domain))
        .header("content-type", "application/json")
        .json(&serde_json::json!({
            "grant_type": "password",
            "client_id": std::env::var("CLIENT_ID").expect("CLIENT_ID not set"),
            "client_secret": std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"),
            "audience": std::env::var("AUTH0_AUDIENCE").expect("AUTH0_AUDIENCE not set"),
            "username": std::env::var("AUTH0_USER_UN").expect("AUTH0_USER_UN not set"),
            "password": std::env::var("AUTH0_USER_PW").expect("AUTH0_USER_PW not set"),
            "scope": "openid profile email",
            "realm": "Username-Password-Authentication"
        }))
        .send()
        .await
        .expect("Failed to get token");

    info!("{:?}", res);
    let token: Auth0TokenResponse = res.json().await.expect("Invalid token response");
    token.access_token
}

Please let me know if any of the proposed solutions work for you!

Kind Regards,
Nik

1 Like

Hi Nik,
Using the full url for the “grant_type” solved the problem. Thank you so much! I’ll post the working code here in case it turns out to be useful to someone else:

async fn get_auth_token() -> String {
    let client = reqwest::Client::new();
    let domain = std::env::var("DOMAIN").expect("DOMAIN not set");
    let res = client
        .post(&format!("{}/oauth/token", domain))
        .header("content-type", "application/json")
        .json(&serde_json::json!({
            "grant_type": "http://auth0.com/oauth/grant-type/password-realm",
            "client_id": std::env::var("CLIENT_ID").expect("CLIENT_ID not set"),
            "client_secret": std::env::var("CLIENT_SECRET").expect("CLIENT_SECRET not set"),
            "audience": &format!("{}/api/v2/", domain),
            "username": std::env::var("AUTH0_USER_UN").expect("AUTH0_USER_UN not set"),
            "password": std::env::var("AUTH0_USER_PW").expect("AUTH0_USER_PW not set"),
            "realm": "Username-Password-Authentication",
            "scope": "openid email profile read:current_user offline_access",
        }))
        .send()
        .await
        .expect("Failed to get token");

    if res.status().is_success() {
        let token: Auth0TokenResponse = res.json().await.expect("Invalid token response");
        token.access_token
    } else {
        "NO TOKEN".to_owned()
    }
}