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_idandclient_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]publicclassSimpleController:ControllerBase{publicIntrospectionClientClient{get;}publicSimpleController(IntrospectionClientclient){Client=client;}[HttpGet]publicasyncTask<IActionResult>Index([FromHeader(Name="Authorization")]stringauthorization){varintrospection=awaitClient.ValidateAuthorization(authorization);if(introspection!=null){varsub=introspection.Subject;varobj=new{hello=sub};returnnewJsonResult(obj);}else{returnnewBearerTokenResult(Client.ClientId);}}}IntrospectionClient gets configuration parameters and http client from dependency injection.
publicIntrospectionClient(IConfiguration configuration,IHttpClientFactory factory){varsection=configuration.GetSection("OAuth2");if(section==null)thrownewApplicationException($"{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
publicasyncTask<OAuth2ServerMetadataModel>GetConfiguration(){varstream=awaitHttp.GetStreamAsync(Issuer+"/.well-known/oauth-authorization-server");returnawaitJsonSerializer.DeserializeAsync<OAuth2ServerMetadataModel>(stream);}This creates OAuth 2.0 Token Introspection request
publicHttpRequestMessageNewIntrospectionRequest(stringintrospectionEndpoint,stringtoken){varhttpRequest=newHttpRequestMessage(HttpMethod.Post,introspectionEndpoint);httpRequest.Headers.Authorization=NewBasicAuthenticationHeader(ClientId,ClientSecret);varintrospectionRequest=newDictionary<string,string>{["token"]=token};httpRequest.Content=newFormUrlEncodedContent(introspectionRequest);returnhttpRequest;}Invoking Token Introspection request
publicasyncTask<IntrospectionResponseModel>InvokeIntrospectionRequest(stringtoken){varmetadata=awaitGetConfiguration();varhttpRequest=NewIntrospectionRequest(metadata.IntrospectionEndpoint,token);varhttpResponse=awaitHttp.SendAsync(httpRequest);if(!httpResponse.IsSuccessStatusCode)returndefault;varstream=awaithttpResponse.Content.ReadAsStreamAsync();returnawaitJsonSerializer.DeserializeAsync<IntrospectionResponseModel>(stream);}This method is used by API controller to validate any token in Authorization header
publicasyncTask<IntrospectionResponseModel>ValidateAuthorization(stringauthorization){if(!TryParseBearerAuthorization(authorization,outvarheader)){returndefault;}varintrospection=awaitInvokeIntrospectionRequest(header.Parameter);if(introspection?.Active==true){returnintrospection;}else{returndefault;}}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 runto run the SimpleAPI application - Use a client to invoke the API (http://localhost:5001/simple)
See DOCKER.md