Skip to content

Resource Owner Password Credentials (ROPC)

This page helps you keep an old app that signs users in with the password grant working, and move it off.

In the password grant, your app asks the user for their email and password, and sends them to the token endpoint, which answers with tokens. The user never sees the auth server’s sign-in page, so your app sees their password, and nothing but a password can be checked.

Turn it on only for an app of your own that signs users in this way and that you can’t change yet, and only for that app’s client, while you move it to the authorization code flow. Leave the global setting off, so no other client gets it by accident. Never turn it on for an app someone else wrote: a user should type their password into the auth server and nowhere else.

  1. In the admin console, open Admin, Clients and click Manage beside your app’s client.

  2. On OAuth2 flows, under Legacy flows (deprecated in OAuth 2.1), set Resource Owner Password Credentials (ROPC) to Enabled, and click Save.

  3. Send the user’s email and password to the token endpoint, with your client’s credentials. A public client leaves client_secret out:

    Terminal window
    curl -X POST https://auth.example.com/auth/token \
    -d grant_type=password \
    -d client_id=legacy-app \
    -d client_secret=my-secret \
    --data-urlencode [email protected] \
    --data-urlencode password=the-users-password \
    -d "scope=openid email"
  4. Read the tokens from the answer:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: no-store
    Pragma: no-cache
    {
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...",
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...",
    "token_type": "Bearer",
    "expires_in": 300,
    "refresh_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...",
    "refresh_expires_in": 2592000,
    "scope": "openid email"
    }
  5. Forget the password as soon as the answer comes back. Use the refresh token when the access token expires, as for any other grant. See Token.

Resource Owner Password Credentials (ROPC) on the OAuth2 flows tab has three choices: Enabled, Disabled, or inherit the global Resource owner password credentials flow enabled under Admin, General, which is off in a new install.

Parameter Required
grant_type Yes password
username Yes The user’s email address. Spaces around it are trimmed, and case doesn’t matter.
password Yes The user’s password
scope No The scopes to ask for, separated by single spaces. openid when left out.
client_id Yes The client’s identifier
client_secret For a confidential client Its secret, in the form body or as Authorization: Basic

The scope can hold the OpenID Connect scopes, such as openid, profile and email, offline_access, and resource:permission scopes the user holds, directly or through a group. A scope the user doesn’t hold is refused, never dropped. An administrative scope needs a client allowed to request one.

  • An access token and a refresh token, always, and an ID token when the scope has openid.
  • An offline refresh token, whether or not your app asked for offline_access, since there’s no session for it to belong to. It follows the offline refresh token’s idle timeout and maximum lifetime, 30 days and a year in a new install. See Refresh tokens.
  • No consent screen. Sending the password counts as the user’s consent to the scope asked for.
  • acr is urn:goiabada:level1 and amr is ["pwd"], whatever the client’s default ACR level, since a password is all that was checked. An API that requires two-factor authentication refuses these tokens by their acr. See ACR and AMR.
  • No sid. The tokens belong to no session, so signing out of the browser doesn’t touch them.
  • auth_time is when the password was checked, and stays that across every refresh.

Each grant leaves a token_issued_ropc_response entry in the audit log. Each invalid_grant below leaves a ropc_auth_failed entry, which records the address only as email_digest.

  • A user with two-factor authentication. The grant can’t ask for a one-time code, so it refuses every user who has an authenticator rather than skip the second factor. They sign in through the browser instead.
  • A disabled user, even with the right password.

Setting the flow to Disabled, for the client or globally, stops new grants and refresh tokens already issued alike: the next refresh is refused with unauthorized_client, and the user has to sign in another way.

RFC 6749 section 4.3.2 requires the endpoint to be protected against guessing. A wrong password counts against the same budgets as one typed into the sign-in page, and the one for each account, 100 wrong passwords an hour, always applies. With the rate limiter on, the password grant also has a budget per IP address for every request, and a tighter one for wrong passwords for one username from one network. See rate limits and Too many attempts or 429.

Errors are JSON, as for every grant, with error and error_description:

error Status When
unauthorized_client 400 The flow is off for the client
invalid_request 400 username, password or client_id is missing, a public client sent a client_secret, or the secret came both in the Authorization header and in the body
invalid_client 401 The client secret is missing or wrong, or the client doesn’t exist or is disabled. The answer carries WWW-Authenticate: Basic realm="goiabada".
invalid_grant 400 The email or password is wrong, the user is disabled, or the user has two-factor authentication
invalid_scope 400 A scope doesn’t exist or the user doesn’t hold it, or an administrative scope the client may not request

A user with two-factor authentication who sends the right password is told why:

{
"error": "invalid_grant",
"error_description": "Resource owner password credentials grant is not available for accounts with two-factor authentication enabled. Please use the authorization code flow instead."
}