Client authentication methods
Client authentication allows an OAuth or OIDC client application to prove its identity to the OneWelcome Identity Platform.
The forms of client authentication include the following:
-
Client secret basic
-
Private key JWT
-
Proof Key for Code Exchange (PKCE)
Client types
Different client types use different forms of client authentication:
-
Confidential clients are applications that are able to securely authenticate with the authorization server. For example, they are able to keep their registered client secret safe.
-
Public clients are unable to use registered client secrets, such as applications running in a browser or on user devices.
The simplest way for confidential clients to prove their identity is to use OAuth basic authentication. Public clients should use PKCE, because they cannot protect the secret. Clients that require a more secure authentication method should use the private_key_jwt method.
Client secret basic
Client secret basic uses a pair consisting of a ClientId and a client secret that are known only to the authorization server and the client as a username and password. These credentials are expected to be sent in the form of either:
-
HTTP basic authorization header
-
URL-encoded form with client credentials in an HTTP POST body
The OAuth 2.0 standard (RFC 6749) recommends using the request header. Only clients that are not capable of sending HTTP request headers should use the HTTP POST body for their credentials.
OAuth 2.0 defines basic authentication as:
[base64(form-urlencoded(client_id) + : + form-urlencoded(client_secret))]
Example
POST /oauth/v1/token HTTP/1.1
Host: token.example.com
Authorization: Basic c2VjcmV0X2FwcDpnYWJpdWdicmVzb2hhZWJob2llcmJnb3dpYWJoYW9oYmE=
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
Private key JWT
A private key JWT replaces the client secret in the token request for an asymmetrically signed JWT. This completely removes the use of shared secrets. Private key JWT signs the token using a private key that only the client application knows, and validates it using a public key that the authorization server knows. This method is defined as part of OpenID Connect.
Example
POST /oauth/v1/token HTTP/1.1
Host: token.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=0862EE05F4602FF63F76D99944816144A2192F73B21B8CCEAD76CC39857CDCAD
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJFUzI1NiJ9.ewogICJzdWIiOiAiMDg2MkVFMDVGNDYwMkZGNjNGNzZEOTk5NDQ4MTYxNDRBMjE5MkY3M0IyMUI4Q0NFQUQ3NkNDMzk4NTdDRENBRCIsCiAgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL29hdXRoIiwKICAiaXNzIjogIjA4NjJFRTA1RjQ2MDJGRjYzRjc2RDk5OTQ0ODE2MTQ0QTIxOTJGNzNCMjFCOENDRUFENzZDQzM5ODU3Q0RDQUQiLAogICJleHAiOiAiMTY4NDExOTQ1OSIsCiAgImlhdCI6ICIxNjc0MTE5NDU5Igp9.VRHgtp7MYaMjgn3LDvDr1Ij3nYrS29eLDFOrbjyhcCBNAqGdObdL3vRI3ZvIeUWe6fQLavzpz55GCj-0Szmfbg
If you decode the token, it has the following header and payload:
{
"alg": "ES256"
}
{
"sub": "0862EE05F4602FF63F76D99944816144A2192F73B21B8CCEAD76CC39857CDCAD",
"aud": "https://example.com/oauth",
"iss": "0862EE05F4602FF63F76D99944816144A2192F73B21B8CCEAD76CC39857CDCAD",
"exp": "1684119459",
"iat": "1674119459"
}
These tokens follow the format defined in RFC 7523 (JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants).
Create a private key JWT
Prepare a payload
Create a JSON formatted payload with the required (and optional) claims. The following claims are available:
Claim | Presence | Type | Description |
---|---|---|---|
aud |
Required | String | The audience of the JWT that uses it to validate the authentication. In this case, it is the base URL of the authorization server, such as https://example.com/oauth |
exp |
Required | Integer | The token expiration time in seconds since January 1, 1970 UTC (UNIX timestamp), such as 1684119459 . This claim fails the request if the expiration time is more than one hour in the future, or if the token has already expired. |
iss |
Required | String | This MUST contain the client_id of the OAuth client. |
sub |
Required | String | This MUST contain the client_id of the OAuth client. |
jti |
Depends | String | The unique token identifier is required for OIDC flows. If you specify this parameter, the token can be used only once, and subsequent token requests don't succeed. |
iat |
Optional | Integer | When the token was issued in seconds since January 1, 1970 UTC (UNIX timestamp), such as 1674119459 . If specified, it must be a time before the request is received. |
Example result:
{
"sub": "0862EE05F4602FF63F76D99944816144A2192F73B21B8CCEAD76CC39857CDCAD",
"aud": "https://example.com/oauth",
"iss": "0862EE05F4602FF63F76D99944816144A2192F73B21B8CCEAD76CC39857CDCAD",
"exp": "1684119459",
"iat": "1674119459"
}
Prepare a JWK set (self-signed)
The OneWelcome Identity Platform supports Elliptic Curve (min P-256) or RSA (min 2048, max 4096 key length) formats.
To generate keys in JWK format, you can use the Java command-line utility created by Justin Richer.
Usage
java -jar json-web-key-generator.jar -t <keyType> [options]
-t,--type <arg> Key Type, one of: RSA, oct, EC, OKP
-s,--size <arg> Key Size in bits, required for RSA and oct key
types. Must be an integer divisible by 8
-c,--curve <arg> Key Curve, required for EC or OKP key type.
For EC keys, must be one of P-256, secp256k1, P-384, or P-521. For OKP keys, must be one of Ed25519, Ed448, X25519, or
X448.
-u,--usage <arg> Usage, one of: enc, sig (optional)
-a,--algorithm <arg> Algorithm (optional)
-i,--id <arg> Key ID (optional), one is generated if not
defined
-g,--idGenerator <arg> Key ID generation method (optional). Can be one
of: date, timestamp, sha256, sha1, none. If
omitted, generator method defaults to
'timestamp'.
-I,--noGenerateId <deprecated> Don't generate a Key ID.
(Deprecated, use '-g none' instead.)
-p,--showPubKey Display public key separately (if applicable)
-S,--keySet Wrap the generated key in a KeySet
-o,--output <arg> Write output to file. Appends to existing
KeySet if -S is used. Key material is not
displayed to console.
-P,--pubKeyOutput <arg> Write public key to separate file. Appends
to existing KeySet if -S is used. Key material
is not displayed to console. '-o/--output'
must also be declared.
-x,--x509 Display keys in X509 PEM format
Example
java -jar json-web-key-generator.jar -t EC -c P-256 -i 1 -u sig -S -x
Example result:
{
"keys": [
{
"kty": "EC",
"d": "0JUmbEAblKfEfvoYyr1b9RtqmqTW_yExZYsBsgHJMko",
"use": "sig",
"crv": "P-256",
"kid": "1",
"x": "ZsitF5jCfGARMx3dip5b62XY0l6_qQm5NZrOKfHu3CQ",
"y": "JlIa2T2TVQR2bVFNrjAsxdcsBAi7aPwDp6Dk4cM1CJ4"
}
]
}
Together with this public key:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZsitF5jCfGARMx3dip5b62XY0l6/
qQm5NZrOKfHu3CQmUhrZPZNVBHZtUU2uMCzF1ywECLto/AOnoOThwzUIng==
-----END PUBLIC KEY-----
Share the public key in the client configuration as a (.pem
) file or a JWKS URI
. Using the JWKS URI
makes it easier to roll out the keys in the future. In the example above, the d
value must be kept private and must not be included in the JWKS URI
.
Generate a JWT
Generate a JWT assertion, including the prepared payload, and sign with the generated private key.
Make the request to the token endpoint
When using the private_key_jwt
method, a client must include the following parameters in a token request:
Parameter | Type | Description |
---|---|---|
client_assertion_type |
String | A type of client_assertion. Its value must be urn:ietf:params:oauth:client-assertion-type:jwt-bearer . |
client_assertion |
String | The signed JWT created in the steps above. |
grant_type . |
String | Type of the grant used, such as client_credentials or authorization_code |
Make the request to other protected endpoints
To send the PrivateKeyJWT
to other endpoints, use bearer token authentication. The client must send this JWT token in the authorization header when making requests to protected resources:
Authorization: Bearer <token>
Public (no authentication)
Client authentication is useful only when a client application can keep a secret. Public clients cannot keep a secret. Examples of those public client applications are single-page applications (SPA) running in the browser, or apps that are installed on the user's device. The same set of credentials are exposed via the browser or stored on the user's device, and anyone who has access to the device is able to obtain these credentials.
This is typically when you would use PKCE instead of client credentials, or a private key JWT. PKCE is not used to authenticate the public client, but to relate the initial authorization request to the token request. PKCE prevents a malicious client from obtaining a token by intercepting the authorization grant.
You could embed some client credentials with the approach of “Why make it easy for them? It’s another hurdle for the attacker”, but when it’s the same set of credentials across all instances of that client application, then the benefits are negligible. The OneWelcome Identity Platform offers a Mobile SDK for Android and iOS that handles dynamic client registration in mobile apps. Each installation of the mobile app has dedicated client credentials.
PKCE
PKCE is an extension to the OAuth 2.0 protocol that prevents authorization code interception attacks. It is a lightweight mechanism that can be implemented in any application that requests an authorization code.
PKCE is particularly beneficial for public clients, such as single-page applications (SPA) and native applications, which cannot securely store a client secret. However, it is also recommended for confidential clients.
For public clients, PKCE is mandatory because it is the only way for the authorization server to ensure the client’s authenticity. For confidential clients, which can safely maintain the confidentiality of their credentials, PKCE is still beneficial. Even though these clients can use a securely stored client secret to authenticate to the server, PKCE provides an additional layer of security. It helps prevent other clients from impersonating your client.
PKCE supports two code challenge methods:
-
Plain: In the plain mode, the code challenge is equal to the code verifier.
-
S256: In S256 mode, the SHA-256 hash of the code verifier is encoded using BASE64URL encoding. The S256 method is recommended because it provides a higher level of security.
Example PKCE request
Here is a simplified example of how PKCE works in a request:
-
The client generates a random string to use as the
code_verifier
. -
The client transforms the
code_verifier
into thecode_challenge
parameter using a supported method (eitherplain
orS256
). -
The client sends the
code_challenge
andcode_challenge_method
to the authorization server’s/authorize
endpoint. -
The authorization server redirects the user to the login and authorization prompt.
-
The authorization server stores the
code_challenge
and redirects the user back to the application with an authorization code. -
The client sends this code and the
code_verifier
to the authorization server’s/token
endpoint. -
The authorization server verifies the
code_challenge
andcode_verifier
. -
The authorization server responds with an ID token and an access token (and optionally, a refresh token).
This way, a malicious attacker can intercept only the authorization code, and they cannot exchange it for a token without the code verifier.
Authorization Code and PKCE: Limitations and Best Practices
Due to security considerations, the authorization endpoint (/authorize
) cannot be called from within an iframe. Depending on the configuration, the API may or may not return HTML content. Regardless, it always includes the X-Frame-Options: DENY
response header.
A comprehensive list of OAuth best practices can be found here.