How simple is an OpenID Connect Basic client? (C#)
Historical note (2024): This post was written in 2012 against an early draft of the OpenID Connect specification. The manual HTTP approach shown here is educational, but modern .NET applications should use
Microsoft.Identity.Webor theIdentityModellibrary rather than hand-rolling these calls. The core concepts — discovery, the authorisation code flow, token endpoint, and JWT claims validation — are still accurate and relevant today.
John Bradley has just posted a great entry demonstrating how simple life is going to be for a Relying Party (RP) when it comes to OpenID Connect. I highly recommend you go and read it.
OpenID Connect provides a lot of advanced facilities to fulfil many additional features requested by the member community. It is full of features that go beyond basic authentication. However, that does not mean that it cannot be used for the simple case of "Just Authentication".
The sample code in John's post is in PHP, so I thought I would quickly provide the same samples in C#.
Key Takeaways
- OpenID Connect Basic requires four pieces of server metadata: client ID, client secret, authorisation endpoint, and token endpoint.
- The authorisation code flow redirects the user to the server; on return the RP exchanges the code at the token endpoint.
- The
id_tokenis a JWT whose payload contains the issuer (iss), subject (user_id), audience (aud), and expiry (exp). - You must validate
iss,aud, andexpbefore accepting anid_token. - In production, always validate the JWT signature — TLS alone is not sufficient.
What does a client need to make an OpenID Connect request?
Before an RP can initiate a login, it needs four pieces of information about the server:
- Client identifier — a unique identifier issued to the RP by the authorisation server (e.g.
3214244). - Client secret — a shared secret used for authenticating the RP at the token endpoint.
- Authorisation endpoint — the server's HTTP endpoint that authenticates the end-user and issues an authorisation code (e.g.
https://server.example.com/authorize). - Token endpoint — the server's HTTP endpoint that issues tokens in exchange for an authorisation code.
In the simplest case, a developer reads the server documentation, pre-registers their application, and receives the above values. A bare-bones login link then looks like this:
<a href="https://server.example.com/authorize?grant_type=code&scope=openid&client_id=3214244&state=af1Ef">
Login with Example.com
</a>The user clicks the link and is taken to the server. If she is not already authenticated, she is prompted for her credentials. Once she agrees to log in to the RP, the server issues a 302 redirect back to the RP's callback URL:
https://client.example.com/cb?code=8rFowidZfjt&state=af1EfNote:
stateprotects against CSRF by binding the request to the browser session. It is recommended by the specification and has been omitted above only to keep the example static.
How do you exchange the code for an id_token?
Now that the RP has the code, it must exchange it at the token endpoint to obtain the id_token. The call uses HTTP Basic Auth with the client_id and client_secret:
var code = Request.Form["code"];
var client = new WebClient();
client.Credentials = new NetworkCredential("testuser", "testpass");
client.Headers.Add("Content-Type", "application/json; charset=utf-8");
var responseJson = client.DownloadString(
new Uri("https://server.example.com/token?code=" + code));The response JSON will look similar to the following:
{
"access_token": "SlAV32hkKG",
"token_type": "Bearer",
"refresh_token": "8xLOxBtZp8",
"expires_in": 3600,
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsInVzZXJfaWQiOiIyNDgyODk3NjEwMDEiLCJhdWQiOiIzMjE0MjQ0IiwiZXhwIjoxMzExMjgxOTcwfQ.eDesUD0vzDH3T1G3liaTNOrfaeWYjuRCEPNXVtaazNQ"
}For basic authentication, the access_token, token_type, and refresh_token fields are not needed. The only field you care about is id_token.
How do you decode the id_token?
The id_token is encoded as a JSON Web Token (JWT). A JWT is three Base64URL-encoded segments separated by periods: header.payload.signature.
Security warning: The code below decodes the payload without verifying the JWT signature. The original post noted that direct TLS delivery from a trusted endpoint reduces the risk, but production code must always validate the JWT signature using the server's public key (obtained from the JWKS endpoint). Skipping signature validation opens the door to token forgery. Use a library such as
Microsoft.IdentityModel.Tokensto handle this correctly.
To extract the payload in C#:
JObject response = JObject.Parse(responseJson);
var token = (string)response["id_token"];
var parts = token.Split('.');
// Base64URL padding fix
var payload = parts[1];
payload = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');
var idBodyBytes = Convert.FromBase64String(payload);
var idBody = System.Text.Encoding.UTF8.GetString(idBodyBytes);The decoded payload will look like this:
{
"iss": "https://server.example.com",
"user_id": "248289761001",
"aud": "3214244",
"iat": 1311195570,
"exp": 1311281970
}What claims must you validate?
The claims in the payload carry specific semantics:
iss— the issuer of the token. Must match the expected issuer for the token endpoint; reject the token if it does not.user_id— the subject identifier, unique within the issuer and never reassigned. When storing user identifiers, always store the(iss, user_id)tuple together.aud— the audience. Must match the RP'sclient_id; reject the token if it does not.iat— the time the token was issued. Can be ignored in this direct flow because the RP is speaking directly to the token endpoint.exp— the expiry time. If the current time is afterexp, the token must be rejected.
A minimal validation method in C#:
private bool CheckId(string idBody, string issuer, string clientId)
{
JObject o = JObject.Parse(idBody);
if ((string)o["iss"] != issuer)
return false;
if ((string)o["aud"] != clientId)
return false;
var exp = (long)o["exp"];
var expiry = DateTimeOffset.FromUnixTimeSeconds(exp);
if (expiry < DateTimeOffset.UtcNow)
return false;
return true;
}Once this validation passes, the RP knows who the user is and the authentication is complete.
Further reading
- John Bradley's original post: http://www.thread-safe.com/2012/07/how-simple-is-openid-connect-basic.html
- If you are working with IdentityServer and encounter certificate or private key errors in .NET, see Solved: Identity Server v3 and 'Invalid provider type specified' CngKey private key errors.
Solution Architect with 30 years in cloud infrastructure, security, identity, and .NET engineering.