Example of a simple OAuth 2.0 protected API. Token introspection is used in this example to validate OAuth 2.0 bearer tokens.
There are two different solutions in this repository. One implemented with ASP.NET Core and an other implemented with Apache HTTP server and mod_auth_openidc.
Examples of clients invoking this api are
- SimpleSPA - JavaScript Single Page application
An OAuth 2.0 Client needs to be configured with information about the OAuth Provider and client credentials. This sample app puts these configuration items into appsettings.json
file as properties of OAuth2
key:
issuer
- name of OAuth Providerclient_id
andclient_secret
- client credentials registered with OAuth Provider
{
"OAuth2": {
"issuer": "https://login.example.ubidemo.com/uas",
"client_id": "api",
"client_secret": "secret"
}
}
Most of the project was generated with Visual Studio. The relevant new or modified files are
This implementation shows what steps are required to create an OAuth 2.0 protected API. A real world application should re-factor token introspection into a middleware component and implement caching of introspection results to improve performance.
Here I'm adding dependency injection service with AddHttpClient
and AddSingleton<IntrospectionClient>
. Then I use AddCors
to setup a default CORS policy.
builder.Services.AddCors(options => options
.AddDefaultPolicy(policy => policy
.AllowAnyOrigin()
.AllowAnyMethod()
.WithHeaders(HeaderNames.Authorization, HeaderNames.Accept, HeaderNames.ContentType)
.WithExposedHeaders(HeaderNames.WWWAuthenticate, HeaderNames.ContentType)));
builder.Services.AddControllers();
builder.Services.AddHttpClient<HttpClient>();
builder.Services.AddSingleton<IntrospectionClient>();
app.UseCors();
app.UseAuthorization();
app.MapControllers();
The API controller gets IntrospectionClient
from dependency injection. For each API request I'm validating the Authorization
header with ValidateAuthorization
.
[Route("simple")]
[ApiController]
public class SimpleController : ControllerBase
{
public IntrospectionClient Client { get; }
public SimpleController(IntrospectionClient client)
{
Client = client;
}
[HttpGet]
public async Task<IActionResult> Index([FromHeader(Name = "Authorization")] string authorization)
{
var introspection = await Client.ValidateAuthorization(authorization);
if (introspection != null)
{
var sub = introspection.Subject;
var obj = new
{
hello = sub
};
return new JsonResult(obj);
}
else
{
return new BearerTokenResult(Client.ClientId);
}
}
}
IntrospectionClient
gets configuration parameters and http client from dependency injection.
public IntrospectionClient(IConfiguration configuration, IHttpClientFactory factory)
{
var section = configuration.GetSection("OAuth2");
if (section == null) throw new ApplicationException($"{nameof(IntrospectionClient)}: Missing configuration OAuth2");
Issuer = section.GetValue<string>("issuer");
ClientId = section.GetValue<string>("client_id");
ClientSecret = section.GetValue<string>("client_secret");
Http = factory.CreateClient();
}
This reads OAuth 2.0 Server Metadata from a well-known address
public async Task<OAuth2ServerMetadataModel> GetConfiguration()
{
var stream = await Http.GetStreamAsync(Issuer + "/.well-known/oauth-authorization-server");
return await JsonSerializer.DeserializeAsync<OAuth2ServerMetadataModel>(stream);
}
This creates OAuth 2.0 Token Introspection request
public HttpRequestMessage NewIntrospectionRequest(string introspectionEndpoint, string token)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Post, introspectionEndpoint);
httpRequest.Headers.Authorization = NewBasicAuthenticationHeader(ClientId, ClientSecret);
var introspectionRequest = new Dictionary<string, string>
{
["token"] = token
};
httpRequest.Content = new FormUrlEncodedContent(introspectionRequest);
return httpRequest;
}
Invoking Token Introspection request
public async Task<IntrospectionResponseModel> InvokeIntrospectionRequest(string token)
{
var metadata = await GetConfiguration();
var httpRequest = NewIntrospectionRequest(metadata.IntrospectionEndpoint, token);
var httpResponse = await Http.SendAsync(httpRequest);
if (!httpResponse.IsSuccessStatusCode) return default;
var stream = await httpResponse.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync<IntrospectionResponseModel>(stream);
}
This method is used by API controller to validate any token in Authorization
header
public async Task<IntrospectionResponseModel> ValidateAuthorization(string authorization)
{
if (!TryParseBearerAuthorization(authorization, out var header))
{
return default;
}
var introspection = await InvokeIntrospectionRequest(header.Parameter);
if (introspection?.Active == true)
{
return introspection;
}
else
{
return default;
}
}
Apache HTTP server and mod_auth_openidc
The following detects CORS simple request and CORS preflight request. For both CORS requests the Access-Control-Allow-Origin
and Access-Control-Expose-Headers
response headers are set. For preflight request in addition the Access-Control-Allow-Headers
header is set and a 204 No Content
response is sent.
See also https://www.w3.org/TR/cors/
<If "-n %{HTTP:Origin}">
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Expose-Headers "WWW-Authenticate"
<If "%{REQUEST_METHOD} == 'OPTIONS' && -n %{HTTP:Access-Control-Request-Method}">
Header always set Access-Control-Allow-Headers "Authorization"
Redirect 204
</If>
</If>
A minimal configuration of mod_auth_openidc, in OAuth 2.0 resource server mode, needs token introspection endpoint and OAuth 2.0 client credentials.
OIDCOAuthIntrospectionEndpoint https://login.example.ubidemo.com/uas/oauth2/introspection
OIDCOAuthClientID api
OIDCOAuthClientSecret secret
OAuth 2.0 resource server integration is declared with AuthType oauth20
.
<Location "/">
AuthType oauth20
Require valid-user
</Location>
Alias /simple ${InstanceRoot}/hello.json
This application is ready to run with Ubisecure SSO at login.example.ubidemo.com.
- Use a client to invoke the API (https://ubi-simple-api.azurewebsites.net/simple)
- Clone this repository
- Install ASP.NET Core SDK from https://www.microsoft.com/net/download
- Use
dotnet run
to run the SimpleAPI application - Use a client to invoke the API (http://localhost:5001/simple)
See DOCKER.md