This article shows how to use a .NET Core console application securely with an API using the RFC 7636 specification. The app logs into IdentityServer4 using the OIDC authorization code flow with a PKCE (Proof Key for Code Exchange). The app can then use the access token to consume data from a secure API. This would be useful for power shell script clients, or .NET Core console apps. Identity.Model.Samples provide a whole range of native client examples, and this code was built using the .NET Core native code example.
Code: https://github.com/damienbod/AspNetCoreWindowsAuth
Native App PKCE Authorization Code Flow
The RFC 7636 specification provides a safe way in which native applications can get access tokens to use with secure applications. Native applications have similar problems to web applications, single sign on is required sometimes, the native apps may not handle passwords, the server requires a way of validating the identity, and the client app requires a way of validating the token and so on.
The RFC 7636 provides one of the best ways of doing this and by using a RFC standard, tested libraries which implement this, can be used. There is no need to re-invent the wheel.
Flow overview src: 1.1. Protocol Flow
The Proof Key for Code Exchange by OAuth Public Clients was designed so that the code cannot be intercepted in the Authorization Code Flow and used to get an access token. This can help for example, when the code is leaked to shared logs on a mobile device and a malicious application uses this to get an access token.
The extra protection is added on this flow by using a code_verifier, code_challenge and a code_challenge_method. The code_challenge and the code_challenge_method are sent to the server with the authorization request. The code_challenge is the derived version of the code_verifier. When requesting the access token, the code_verifier is sent to the server, and this is then validated on the OIDC server using the values sent in the orignal authorization request.
STS Server Configuration
On IdentityServer4 ,the Proof Key for Code Exchange by OAuth can be configured as follows:
new Client { ClientId = "native.code", ClientName = "Native Client (Code with PKCE)", RedirectUris = { "http://127.0.0.1:45656" }, PostLogoutRedirectUris = { "http://127.0.0.1:45656" }, RequireClientSecret = false, AllowedGrantTypes = GrantTypes.Code, RequirePkce = true, AllowedScopes = { "openid", "profile", "email", "native_api" }, AllowOfflineAccess = true, RefreshTokenUsage = TokenUsage.ReUse }
The RequirePkce is set to true, and no secrets are used, unlike the Authorization Code flow for web applications, as it makes no sense on public mobile native devices. Depending on the native device, the RedirectUris can be configured as required.
Native client using .NET Core
Implementing the client for a .NET Core application is really easy thanks to the IdentityModel.OidcClient nuget package and the examples provided on github. This repo provides reference examples for lots of different native client types, really impressive.
This example was built used the following project: .NET Core Native Code
IdentityModel.OidcClient takes care of the PKCE handling and the flow.
The login can be implemented as follows:
private static async Task Login() { var browser = new SystemBrowser(45656); string redirectUri = "http://127.0.0.1:45656"; var options = new OidcClientOptions { Authority = _authority, ClientId = "native.code", RedirectUri = redirectUri, Scope = "openid profile native_api", FilterClaims = false, Browser = browser, Flow = OidcClientOptions.AuthenticationFlow.AuthorizationCode, ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect, LoadProfile = true }; _oidcClient = new OidcClient(options); var result = await _oidcClient.LoginAsync(new LoginRequest()); ShowResult(result); }
The SystemBrowser class uses this implementation from the IdentityModel.OidcClient samples. The results can be displayed as follows:
private static void ShowResult(LoginResult result) { if (result.IsError) { Console.WriteLine("\n\nError:\n{0}", result.Error); return; } Console.WriteLine("\n\nClaims:"); foreach (var claim in result.User.Claims) { Console.WriteLine("{0}: {1}", claim.Type, claim.Value); } Console.WriteLine($"\nidentity token: {result.IdentityToken}"); Console.WriteLine($"access token: {result.AccessToken}"); Console.WriteLine($"refresh token: {result?.RefreshToken ?? "none"}"); }
And the API can be called then using the access token.
private static async Task CallApi(string currentAccessToken) { _apiClient.SetBearerToken(currentAccessToken); var response = await _apiClient.GetAsync(""); if (response.IsSuccessStatusCode) { var json = JArray.Parse(await response.Content.ReadAsStringAsync()); Console.WriteLine(json); } else { Console.WriteLine($"Error: {response.ReasonPhrase}"); } }
The IdentityModel.OidcClient can be used to implement almost any native device which needs to or should implement the Proof Key for Code Exchange by OAuth for authorization. There is no need to do the password handling in your native application.
Links:
http://openid.net/2015/05/26/enhancing-oauth-security-for-mobile-applications-with-pkse/
https://tools.ietf.org/html/rfc7636
https://github.com/IdentityModel/IdentityModel.OidcClient.Samples
https://connect2id.com/blog/connect2id-server-3.9
https://www.davidbritch.com/2017/08/using-pkce-with-identityserver-from_9.html
https://developer.okta.com/authentication-guide/implementing-authentication/auth-code-pkce
https://community.apigee.com/questions/47397/why-do-we-need-pkce-specification-rfc-7636-in-oaut.html