2021年1月29日 星期五

[Golang] Install and Get started with samples

  Golang   Get Stared    Install Golang   




 

Introduction


 

Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

See more details on golang.org.

 

Learn how to install Go, create a Hello world and some samples in the following links.


Install Go

 

1.  Install on Windows

2.  Install on Ubuntu

 

 

Hello World

 

This is the tutorial from official golang.org. 

I shorted it up in this article.



A TODO website

 

In this tutorial, we will use http package to create a TODO list website that can

  • List done and undone TODOs
  • Create new TODO
  • Remove a TODO

[.Net 5] Upgrade to NET 5 issues

 

 .NET 5   Upgrade issues   

 

 

Trouble shooting


 

 

The "RazorGenerate" task failed unexpectedly

 

After installing .NET 5 SDK, using Visual Studio 2019 (Version 16.8.3) on Windows to build .NET Core 2.2 project will get the error:

 

The "RazorGenerate" task failed unexpectedly

 

 

To resolve the issue, following the steps (Reference: https://stackoverflow.com/a/64834683/7045253 ).

 

1.  Set DOTNET_HOST_PATH environment variable.

$ setx DOTNET_HOST_PATH "%ProgramFiles%\dotnet\dotnet.exe"



2.  Delete C:\Program Files\dotnet\sdk\NuGetFallbackFolder.

3.  Restart your PC.

 

 

 

 

2021年1月23日 星期六

[ASP.NET Core] Identity Server 4 - Get authorized user claims

   ASP.NET Core   JW  Claims  






Introduction

 

Sometimes we want to get the user’s information by JWT before go inside the API controller/action.

For example, verify the JWT claim(s) or even overwrite the payload.

 

In this short article, we will create a custom Action Filter which inherits/implements Attribute and IAsyncActionFilter that can:

 

·         Get user claims.

·         Also: Get Http request’s request and modify it.

·         Also: Save custom items to HttpContext.

 

 

 

Related articles

 

01.      [OpenLDAP] Create an OpenLDAP container

02.      [ASP.NET Core] Identity Server 4 – Concepts

03.      [ASP.NET Core] Identity Server 4 – LDAP authentication

04.      [ASP.NET Core] Identity Server 4 – Secure Web API

05.      [ASP.NET Core] Identity Server 4 – Custom Event Sink

06.      [ASP.NET Core] Identity Server 4 – Refresh Token

07.      [ASP.NET Core] Identity Server 4 – Role based authorization

08.      [ASP.NET Core] Identity Server 4 – Policy based authorization

09.      [ASP.NET Core] Identity Server 4 - Dockerize

10.      [ASP.NET Core] Identity Server 4 – Client Credential

11.      [ASP.NET Core] Identity Server 4 – Policy based authorization with custom Authorization Handler

12.      [ASP.NET Core] Identity Server 4 – Signing credential

13.      [ASP.NET Core] Identity Server 4 – Authenticate by multiple LDAP

14.      [ASP.NET Core] Identity Server 4 – Cache and refresh Discovery document

15.      [ASP.NET Core] Identity Server 4 – PKCE Authorization Code Flow

16.      [ASP.NET Core] Identity Server 4 – PKCE Authorization Code Flow (Javascript client)

 .NET Core] Identity Server 4 – Get .NET Core] Identity Server 4 – Get authorized user claims

 [ASP.NET Core] Identity Server 4 – Get authorized user claims

[ASP.NET Core] Identity Server 4 – Get authorized use

 

Environment

 

Docker 18.05.0-ce

ASP.NET Core 3.1.300

IdentityServer4 3.1.2

IdentityModel 3.10.10

 

 

 

Implement

 

The source code is on my Github.

 

Create User Profile filter

 

There are two ways to get the user’s claims:

 

1.  Use System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler to decode the JWT.

2.  Use the System.Security.ClaimsPrinciple from HttpContext.

 

UserProfileFilter


public class UserProfileFilter : AttributeIAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext contextActionExecutionDelegate next)
    {
        string uid = string.Empty;
        string email = string.Empty;
        StringValues authHeaderVal = default(StringValues);

        // Method 1. Use System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler to decode the JWT.
        if (context.HttpContext.Request.Headers.TryGetValue("Authorization"out authHeaderVal))
        {
            string bearerTokenPrefix = "Bearer";
            string accessToken = string.Empty;
            string authHeaderStr = authHeaderVal.ToString();
            if (!string.IsNullOrEmpty(authHeaderStr) && authHeaderStr.StartsWith(bearerTokenPrefixStringComparison.OrdinalIgnoreCase))
            {
                accessToken = authHeaderStr.Replace(bearerTokenPrefixstring.EmptyStringComparison.OrdinalIgnoreCase).Trim();
            }

            var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
            var token = handler.ReadJwtToken(accessToken);
            uid = token.Claims.FirstOrDefault(c => c.Type.Equals("sub"StringComparison.OrdinalIgnoreCase))?.Value;
            email = token.Claims.FirstOrDefault(c => c.Type.Equals(JwtClaimTypes.Email))?.Value;
        }

        // 2. Use the System.Security.ClaimsPrinciple from HttpContext.
        var user = context.HttpContext.User;
        if (user.Identity.IsAuthenticated)
        {
            uid = user.Claims.FirstOrDefault(c => c.Type.Equals("sub"StringComparison.OrdinalIgnoreCase))?.Value;
            email = user.Claims.FirstOrDefault(c => c.Type.Equals(JwtClaimTypes.Email))?.Value;
        }
        
        await next();
    }
}



The above sample code shows how to get user’s sub and email claims.

 

 

Use it in Controller/Action

 


 [HttpGet("UserProfile")]
 [Authorize]
 [TypeFilter(typeof(UserProfileFilter))]
 public async Task<IActionResultUserProfile()
 {
     return this.Ok();
 }

 



Also: Get request’s payload and modify it

 

We can use ActionArguments to get the payload object of a Http request and modify it before the payload get into the Controller’s Action.

 

UserProfileFilter

 

 public class UserProfileFilter : AttributeIAsyncActionFilter
 {
    public async Task OnActionExecutionAsync(ActionExecutingContext contextActionExecutionDelegate next)
    {
            // ...skip

            MyPayload payload = (MyPayload)context.ActionArguments?.Values.FirstOrDefault(v => v is RequestDto);
            payload.Uid = uid;
            
            await next();
    }
 }

 




Also: Save custom items to HttpContext

 

Once we what to keep some information on the current request, we can easily save them as HttpContext items and get them later inside Controller’s Action.

 

UserProfileFilter


public class UserProfileFilter : AttributeIAsyncActionFilter    
{
    public async Task OnActionExecutionAsync(ActionExecutingContext contextActionExecutionDelegate next)
    {
        // ...skip
        string itemName = "UserProfile";
        context.HttpContext.Items.Add(itemNamenew { Id = uidEmail = email });
            
        await next();
    }
 }

 

 

Now we can get the items as following,

 

Controller


[HttpGet("UserProfile")]
 [TypeFilter(typeof(UserProfileFilter))]
 public async Task<IActionResultUserProfile()
 {
     if (this.HttpContext.Items.TryGetValue("UserProfile"out object userProfile))
     {
         this.logger.LogInformation(JsonConvert.SerializeObject(userProfile));
     }
     
return this.Ok();
 }



  

Source Code

 

Github: KarateJB/AspNetCore.IdentityServer4.Sample

 

 

 

Reference

 

Overwrite request object in ASP .NET Core

 

 

 

2021年1月4日 星期一

[ASP.NET Core] Identity Server 4 – PKCE Authorization Code Flow

  ASP.NET Core   Identity Server 4   Authorization Code    PKCE  

 




Introduction


 

If our web application in on server side, e.q. SSR. We can use Authorization Code Flow to exchange an Authorization Code for tokens.

The flow is as following,

(Picture from auth0.com)

 

As we can see, the flow:

1.  Pass the Authorization Code thru URL/URL schema.

2.  Use Authorization Code + Client ID + Client Secret to get the id_token and access_token.

 

 

However the Authorization Code may be hacked, also in public client, such as native mobile application or SPA (single page application), the Client Secret is hardly to be protected.

So it is suggested to integrate PKCE (Proof Key for Code Exchange) with Authorization Code Flow.

 

The PKCE Authorization Code flow was specified in RFC7636 and its flow is as following,

 

 

In this tutorial, we will implement the PKCE Authorization Code Flow with cookie-based authorization that is based on Identity Server 4.

 

Here is the final result’s demo.

 

 

 

 

 

Environment


 

Docker 18.05.0-ce

.NET Core SDK 3.1.201

IdentityServer4 3.1.2

IdentityModel 4.0.0

 

 

 

Implement


 

The source code is on my Github.

 

PKCE Client configuration on Auth Server

 

Go to Idsrv4 project (Auth Server), and open InMemoryInitConfig.cs to set the below configuration on a new client:

 

 

InMemoryInitConfig.cs

 

Notice that there are some key settings:

 

Configuration

Description

Value

RedirectUris

The allowed Uri(s) to return Authorization Code or Tokens.

E.q. "https://client-app/signin-oidc"

RequireConsent

If enabled, the user will be redirected to the consent page after sign-in.

Boolean, default: true.

RequirePkce

Whether a proof key is a must for requesting Authorization Code.

Boolean, default: false.

AllowPlainTextPkce

Send a proof key by plain method.

(not recommended)

Boolean, default: false.

 

public static IEnumerable<Client> GetClients()
 {
            return new[]
            {
                new Client
                {
                    ClientId = "PkceCodeBackend",
                    ClientName = "PKCE Authorization code Client",
                    AllowedGrantTypes = GrantTypes.Code,
                    AccessTokenType = AccessTokenType.Jwt,
                    ClientSecrets = { new Secret("secret".Sha256()) },
                    RedirectUris = {
                        "https://localhost:5001/signin-oidc"
                    },
                    RequireConsent = true// If enable, will redirect to consent page after sign-in
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "MyBackendApi2"
                    },
                    AllowOfflineAccess = true,
                    RequirePkce = true,
                    AllowPlainTextPkce = false,

                    AccessTokenLifetime = AppSettingProvider.Global?.AccessToken?.DefaultAbsoluteExpiry ?? 3600,
                    RefreshTokenUsage = TokenUsage.OneTimeOnly, // Or ReUse
                    RefreshTokenExpiration = TokenExpiration.Sliding,
                    AbsoluteRefreshTokenLifetime = AppSettingProvider.Global?.RefreshToken?.DefaultAbsoluteExpiry ?? 360000,
                    SlidingRefreshTokenLifetime = AppSettingProvider.Global?.RefreshToken?.DefaultSlidingExpiry ?? 36000,

                    ClientClaimsPrefix = string.Empty,
                }
           }
 }

 

 

Enable cookie-based authentication and Authorization Code Flow on client

 

Startup.cs: ConfigureServices

public void ConfigureServices(IServiceCollection services)
        {
            // … skip

            services.AddAuthentication(options =>
                    {
                        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; // "Cookies"
                        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; // "Cookies"
                        options.DefaultChallengeScheme = "oidc";
                    })
                    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddOpenIdConnect("oidc", options =>
                    {
                        const string CODE_VERIFIER_KEY = "code_verifier";
                        const string CODE_CHALLENGE_KEY = "code_challenge";
                        const string CODE_CHALLENGE_METHOD_KEY = "code_challenge_method";

                        // Get config values from AppSetting file
                        string oidcServerBaseUrl = appSettings?.Host.OidcServer;
                        bool isRequireHttpsMetadata = !string.IsNullOrEmpty(oidcServerBaseUrl) && oidcServerBaseUrl.StartsWith("https");
                        options.Authority = string.IsNullOrEmpty(oidcServerBaseUrl) ? "https://localhost:6001" : oidcServerBaseUrl;
                        options.RequireHttpsMetadata = isRequireHttpsMetadata;
                        options.MetadataAddress = $"{oidcServerBaseUrl}/.well-known/openid-configuration";
                        options.BackchannelHttpHandler = AuthMetadataUtils.GetHttpHandler();

                        options.ClientId = "PkceCodeBackend";
                        options.ClientSecret = "secret";
                        options.ResponseType = "code";
                        options.ResponseMode = "form_post";
                        options.CallbackPath = "/signin-oidc";

                        options.SaveTokens = true;
                        options.Scope.Add(appSettings?.AuthOptions?.Audience);
                        options.Scope.Add("offline_access"); // Get refresh token

                        options.Events.OnRedirectToIdentityProvider = context =>
                                    {
                                        // only modify requests to the authorization endpoint
                                        if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
                                        {
                                            // generate code_verifier
                                            var codeVerifier = CryptoRandom.CreateUniqueId(32);

                                            // store codeVerifier for later use
                                            context.Properties.Items.Remove(CODE_VERIFIER_KEY);
                                            context.Properties.Items.Add(CODE_VERIFIER_KEY, codeVerifier);

                                            // create code_challenge
                                            string codeChallenge;
                                            using (var sha256 = SHA256.Create())
                                            {
                                                var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
                                                codeChallenge = Base64Url.Encode(challengeBytes);
                                            }

                                            // add code_challenge and code_challenge_method to request
                                            context.ProtocolMessage.Parameters.Remove(CODE_CHALLENGE_KEY);
                                            context.ProtocolMessage.Parameters.Remove(CODE_CHALLENGE_METHOD_KEY);
                                            context.ProtocolMessage.Parameters.Add(CODE_CHALLENGE_KEY, codeChallenge);
                                            context.ProtocolMessage.Parameters.Add(CODE_CHALLENGE_METHOD_KEY, "S256");
                                        }

                                        return Task.CompletedTask;
                                    };

                        options.Events.OnAuthorizationCodeReceived = context =>
                        {
                            // only when authorization code is being swapped for tokens
                            if (context.TokenEndpointRequest?.GrantType == OpenIdConnectGrantTypes.AuthorizationCode)
                            {
                                // get stored code_verifier
                                if (context.Properties.Items.TryGetValue(CODE_VERIFIER_KEY, out var codeVerifier))
                                {
                                    // add code_verifier to token request
                                    context.TokenEndpointRequest.Parameters.Add(CODE_VERIFIER_KEY, codeVerifier);
                                }
                            }

                            return Task.CompletedTask;
                        };
                    });
        }

 

 

Now we can only add [Authorize] to the protected route(s) as following,

[Route("[controller]")]
[Authorize]
public class OpenIdController : Controller
{ }



This will result in redirecting to the Authentication prompt (sign-in page) in Auth Server.

 

 

Auth Server’s Sign-in and Consent pages

 

We can follow the idsrv4 template to create the sign-in and consent pages on Auth Server.

I refactored some of the codes from the template in my sample code.

 

Sign-in page

 

 

 

Consent page

 


How the flow steps through

 

I will show what happens during the authentication flow.


 

1.  When a user who is not signed in, the client application will first send Authorization Code request to Auth Server’s URL: /connect/authorize with a random Code Challenge (URL safe base64 string).





The /connect/authorize url contains the following parameters.


(Table 1.)

URL parameter

Value

Description

client_id

PkceCodeBackend

The Client Id

redirect_uri

https://localhost:5001/signin-oidc

 

response_type

code

 

Scope

openid profile MyBackendApi2 offline_access

 

code_challenge_method

S256

 

code_challenge

uc6gNUTWAnwgwAo4O3QCRxLw8fFniR36vrA_2WMPxQo

URL safe base64

response_mode

form_post

 

Nonce

637455834911251586.ZDNhN…

Nonce is used to associate a Client session with an ID Token. It serves as a token validation parameter to mitigate replay attacks. See OpenID Connect specification.

State

CfDJ8LH7J_bUBzNAlMjrmdBfJia0f…

State is to protect the end user from cross site request forgery (CSRF) attacks. See OAuth 2.0 protocol RFC6749.

x-client-SKU

ID_NETSTANDARD2_0

The metadata of Identity Server 4.

x-client-ver

5.5.0.0

The metadata of Identity Server 4.



Then Auth Server will redirect the user to /Account/Login?RedirectUrl= with the url parameter: RedirectUrl.

The RedirectUrl contains the URL encoded string as following.

If you decode the value, you will get exactly the same information as the Table 1.

URL parameter

Value

Description

ReturnUrl

%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3DPkceCodeBackend%26redirect_uri%3Dhttps%253A%252F%252F172.23.131.181%253A5001%252Fsignin-oidc%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520MyBackendApi2%2520offline_access%26code_challenge_method%3DS256%26code_challenge%3Duc6gNUTWAnwgwAo4O3QCRxLw8fFniR36vrA_2WMPxQo%26response_mode%3Dform_post%26nonce%3D6374558349BAXebbSohHgE%26x-client-SKU%3DID_NETSTANDARD2_0%26x-client-ver%3D5.5.0.0 HTTP/1.1

URL encoded string



In this step, the user had been redirected to the login page of Auth Server.

2.  User entered his/her ID/PWD and submitted. The Auth Server authenticated the user and redirect to the consent page, then the user submitted on the consent page.
In this step, the following urls (inside red block) contains the same information as Table 1.




3.  Finally, the Auth Server sent back the Authorization Code thru the redirect_url: https://localhost:5001/signin-oidc.






And the user was redirected to the original url on client side: /OpenId/Login.





And client side used the Authorization Code + Code Verifier to get the access token thru Auth Server’s API: /connect/token.










 

Source Code

 

Github: KarateJB/AspNetCore.IdentityServer4.Sample

 

 

 

Reference


 

Authorization Code Flow with Proof Key for Code Exchange (PKCE)