OAuth Troubleshooting for Developers

OAuth Troubleshooting for Developers

Intro

When integrating OAuth security into apps, developers will be interfacing with a sophisticated new component, the Authorization Server. Its support for security standards enables customers to implement their security use cases with only simple code. Developers must learn flows, and use multiple HTTPS endpoints, while also understanding configuration settings that influence behavior.

When implementing a new security flow it is common to make mistakes before you get it right. The Authorization Server must then report errors using the same techniques as your own APIs. This article summarizes the main error behavior from OAuth standards, and a general approach for OAuth troubleshooting, to ensure that developers remain productive, and avoid ever being blocked for long.

OAuth Errors

The original OAuth Specification in RFC6749 specifies that when invalid requests are sent, the Authorization Server should return error responses to applications. A string error code must be returned in the error field, and an optional more detailed message can be returned in an error_description field.

Consider the folowing simple client credentials grant request to the Authorization Server's token endpoint. This can be run using the curl tool, and you can produce an error by sending an invalid client ID, client secret or scope:

curl -k -X POST https://localhost:8443/oauth/v2/oauth-token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=client-credentials-client" \
    -d "client_secret=Password1" \
    -d "grant_type=client_credentials" \
    -d "scope=readd"

The Authorization Server can then return a confidential error response with only an error field, for the most secure setup in production systems. The codes in error responses are sufficient to convey distinct causes to client applications:

{"error":"invalid_client"}
{"error":"invalid_scope"}

The Authorization Server may provide an option to return more detailed errors in error responses, in which case you may receive a response of the following form, to better explain the cause. The Curity Identity Server has an option called Expose Detailed Error Messages, which is usually activated in development environments.

{"error":"invalid_client", "error_description":"Client or client authentication not configured on profile"}
{"error":"invalid_scope", "error_description":"No valid scope was requested"}

Code Flow Errors

Web and mobile apps use the Code Flow, and front channel error responses returned to the browser work a little differently. Error values are returned to owned URLs, most commonly as an HTTP GET request, with URL encoded error details:

https://www.example.com/callback?error=unsupported_response_type&error_description=Unknown+or+invalid+response+type+requested

Error codes are only returned to the application if a trusted Client ID and Redirect URI are supplied. If this is not the case, the Authorization Server will instead present an error page, and this can also happen for multiple other reasons. In some cases this will present an explanatory message, and it should also present an identifier that correlates the occurrence to logs.

Error Display

Developer Logging

The Authorization Server must never return sensitive data to clients, so the error and error_description fields are often insufficient to fully understand a problem. Therefore developers need to access useful logs, and during development a fairly detailed log level is useful.

The Curity Identity Server has an environment variable of LOGGING_LEVEL=DEBUG that is designed for developers to use when implementing a new flow, so that they get detailed error logs. An example log entry is shown below, for a JWT Client Assertion that failed cryptographic validation.

idsvr-curity-idsvr-1       | 2022-04-28T11:24:05:258+0000 DEBUG MdclinHH afe7164f {req-105} se.curity.identityserver.tokens.issuers.ClientAssertionTokenIssuer - Invalid JWT encountered while trying to verify signature: JWT rejected due to invalid signature. Additional details: [[9] Invalid JWS Signature: JsonWebSignature{"kid":"1","alg":"PS256"}

This type of log entry should contain sufficient detail that developers can solve their own problem in a timely manner, so that in most cases there is no need to resort to measures such as contacting the vendor or asking online questions.

Error Codes

The majority of errors, such as invalid_scope or unsupported_response_type, should be ironed out during development. Once the app is reliably coded these will not occur in production. Some errors are expiry related though, and will occur in normal usage, as for the following examples:

Error CodeMeaning
login_requiredThe client application requested a Single Sign On operation but the user's authenticated session is expired
invalid_grantThis will periodically occur when using a refresh token to renew an access token, if the refresh token itself is expired

It is recommended to be prepared for unexpected error codes being returned to the application though. In some cases you may need to present an error display within your own apps, and it can be useful to include the Authorization Server's error code here.

HTTP Messages

OAuth requests are often sent as the result of using third party security libraries, and it can be highly useful to view the full HTTP request and response details. Some OAuth messages require form-URL-encoded request bodies, and a common mistake is to send JSON request bodies instead. Use of browser or HTTP proxy tools to view in-flight requests will enable you to quickly resolve this type of problem. The following details were traced using the open source mitmproxy tool:

https_proxy=http://127.0.0.1:8080 \
curl -k -X POST https://localhost:8443/oauth/v2/oauth-token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=client-credentials-client" \
    -d "client_secret=Password1" \
    -d "grant_type=client_credentials" \
    -d "scope=readd"

HTTP Message

External Authentication Errors

The Authorization Server enables you to build solutions where users authenticate in many different ways. When connecting to an external system such as Facebook, this will involve OpenID Connect messages to the external system's token endpoint and userinfo endpoint, as well as external ID token validation. There is plenty of scope for these connections to fail, so ensure that the Authorization Server reports them clearly when getting integrated.

idsvr-local-curity-idsvr-1  | se.curity.identityserver.sdk.errors.ExternalServiceException: Facebook user-info endpoint returned error: 400
idsvr-local-curity-idsvr-1  | 	at se.curity.identityserver.errors.DefaultExceptionFactory.externalServiceException(DefaultExceptionFactory.java:221) ~[identityserver.core-7.0.1-d4913ceb4f.jar:7.0.1-d4913ceb4f]
idsvr-local-curity-idsvr-1  | 	at se.curity.identityserver.plugin.authenticator.social.facebook.logic.FacebookAuthenticatorLogic.getUserInfoAsJson(FacebookAuthenticatorLogic.java:152) ~[identityserver.plugins.authenticators.social.facebook-7.0.1-d4913ceb4f.jar:7.0.1-d4913ceb4f]
idsvr-local-curity-idsvr-1  | 	at se.curity.identityserver.plugin.authenticator.social.facebook.logic.FacebookAuthenticatorLogic.authenticate(FacebookAuthenticatorLogic.java:98) ~[identityserver.plugins.authenticators.social.facebook-7.0.1-d4913ceb4f.jar:7.0.1-d4913ceb4f]

Server Errors

Exceptions can occur when a third party component that the Authorization Server relies upon is unavailable, leading to a failure in a JDBC, LDAP or HTTP connection. In this case you should have access to a stack trace that explains the underlying cause, which is usually configuration or infrastructure related.

org.postgresql.util.PSQLException: FATAL: password authentication failed for user "postgres"
	at org.postgresql.core.v3.ConnectionFactoryImpl.doAuthentication(ConnectionFactoryImpl.java:525) ~[?:?]
	at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:146) ~[?:?]
	at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:197) ~[?:?]
	at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:49) ~[?:?]
	at org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:217) ~[?:?]
	at org.postgresql.Driver.makeConnection(Driver.java:458) ~[?:?]
	at org.postgresql.Driver.connect(Driver.java:260) ~[?:?]
	at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~[?:?]
	at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:364) ~[?:?]
	at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206) ~[?:?]
	at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:476) ~[?:?]

Handling Expiry Events

Developers must handle expiry events in client applications:

  • Access tokens are short lived, with a lifetime such as 15 minutes. When they expire, the client application will receive a 401 status response from APIs. If the client has a refresh token, it is then used to try to silently refresh the access token by calling the Authorization Server, then retrying the original API call with the new access token.

  • The most secure way to use refresh tokens is to make them single use. Every time a new access token is returned, a new refresh token is also returned. Refresh tokens also expire, in which case the refresh attempt will return an error response with an invalid_grant error code. The client application must then prompt the user to sign in again.

User facing apps often have multiple views, which call API endpoints at the same time. Views usually need to coordinate when performing actions such as token refresh or starting a login, to ensure that these events only occur once. If you are new to OAuth, these behaviors are a little tricky to code, so it is recommended to invest in some shared code that handles them reliably.

Rehearsal

To ensure resilient apps, it is recommended to rehearse expiry and error events as part of the development process. This fits nicely into modern people collaboration processes, to ensure that developers understand how DevOps Troubleshooting will work and vice versa.

Exercises such as ensuring that the app behaves as expected if left running overnight, or if the Authorization Server is intentionally misconfigured, also help to ensure that the app deals best with unexpected scenarios if they ever occur in production environments.

Conclusion

Integrating OAuth flows into applications is not trivial and there is a learning curve. When integrating new flows there will be some invalid requests and developers will need to resolve failures. When choosing an Identity and Access Management system, ensure that it provides modern troubleshooting capabilities that help to enable productive development.