2021年4月9日 星期五

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

 ASP.NET Core   Identity Server 4   Authorization Code    PKCE   JS  

 



 

 

 

Introduction


 

For how PKCE Authorization Code Flow works, you can have a look on my previous article:  [ASP.NET Core] Identity Server 4 – PKCE Authorization Code Flow.

The flow is as following, we will focus on how to create a JavaScript client that can authenticate user by Identity Server 4 and access the backend’s resource with a given Access Token.




 

 

Notice that I put the JavaScript client at the Backend Server to make the sample code simple (As the following figure).

In the real world, we have to separate the Javascript client (Client) and the Backend Server (Resource server), see reference: [ASP.NET Core] Identity Server 4 – Concepts.



 


Here is the final result’s demo.


 

 


 

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)

 

 

 

 

Environment


 

Docker 18.05.0-ce

.NET Core SDK 3.1.201

IdentityServer4 3.1.2

oidc-client.js 1.11.5

 

 

 

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

AllowedGrantTypes

Grant type

GrantTypes.Code

RequirePkce

Specifies whether a proof key is required for authorization code based token requests (defaults to false).

true

RequireClientSecret

If set to false, no client secret is needed to request tokens at the token endpoint.

false

RedirectUris

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

 

PostLogoutRedirectUris

Specifies allowed URIs to redirect to after logout

 

AllowedCorsOrigins

Gets or sets the allowed CORS origins for JavaScript clients.

 

 

public static IEnumerable<Client> GetClients ()
{
     return new [] {
            new Client {
                ClientId = "PkceJS",
                    ClientName = "JavaScript Client",
                    AllowedGrantTypes = GrantTypes.Code,
                    RequirePkce = true,
                    RequireClientSecret = false,
                    RedirectUris = { "https://localhost:5001/OpenId/Login/JS" },
                    PostLogoutRedirectUris = { "https://localhost:5001/OpenId/Login/JS" },
                    AllowedCorsOrigins = { "https://localhost:5001" },
                    AllowedScopes = {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        ApiResources.MyBackendApi2
                    },
                    AllowOfflineAccess = true,
                    AccessTokenLifetime = 3600,
                    RefreshTokenUsage = TokenUsage.OneTimeOnly// Or ReUse
                    RefreshTokenExpiration = TokenExpiration.Sliding,
                    AbsoluteRefreshTokenLifetime = 360000,
                    SlidingRefreshTokenLifetime = 36000,
                    ClientClaimsPrefix = string.Empty,
            }
     }
}


 

We have to implement the AccountController in Identity Server, the code is as same as

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

 

Or you can see the full code on Github.

 

 

Client side

 

Since we put the JavaScript client on Backend project, which is based on ASP.NET Core.

We have to do some initialization works to enable related JS files.

 

First, install oidc-client.js with NPM.

 

$ cd src/AspNetCore.IdentityServer4.WebApi
$ npm install oidc-client --save

 

 

Copy node_modules/oidc-client/dist/oidc-client.js (or oidc-client.min.js) to wwwroot/js/.

And create the following js files:


File name

Directory

Description

app.js

wwwroot/js

The main javascript to run thru the authentication flow

app-config.js

wwwroot/js

Set the client and identity server’s host URLs, that will be used for redirect URLs after logged in or out.

 

 

The js file structure is as following for reference.

├── node_modules
|  ├── oidc-client
|  |  ├── dist
|  |  |  ├── oidc-client.d.ts
|  |  |  ├── oidc-client.js
|  |  |  ├── oidc-client.min.js
|  |  |  ├── oidc-client.rsa256.slim.js
|  |  |  ├── oidc-client.rsa256.slim.min.js
|  |  |  ├── oidc-client.slim.js
|  |  |  └── oidc-client.slim.min.js
└── wwwroot
   ├── js
   |  ├── app-config.js
   |  ├── app.js
   |  └── oidc-client.js

 

Before we implement the client logic, we have to enable static file serving.

 

Startup.cs: Configure

 

public void Configure(IApplicationBuilder app)
{
        // Use static files
        app.UseStaticFiles();
}

 

 

 

Views/LoginByJs.cshtml

 

 

Let’s create a View as the main page, which will import the js files we had just created.

<h2 id="welcome_msg">
    Welcome!  <label id="uid"></label><button class="btn btn-warning" id="logout">Sign Out</button>
    <button class="btn btn-success" id="api">Test secured API</button>
</h2>
<h2 id="signin_msg">
    Welcome! Please <button class="btn btn-primary" id="login">Sign In</button> first.
</h2>
<table class="table">
    <thead class="thead-dark">
        <tr>
            <th>#</th>
            <th>Key</th>
            <th>Value</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row">1</th>
            <td>id_token</td>
            <td><label id="id_token"></label></td>
        </tr>
        <tr>
            <th scope="row">2</th>
            <td>access_token</td>
            <td><label id="access_token"></label></td>
        </tr>
        <tr>
            <th scope="row">3</th>
            <td>refresh_token</td>
            <td><label id="refresh_token"></label></td>
        </tr>
        <tr>
            <th scope="row">4</th>
            <td>expires_at</td>
            <td><label id="expires_at"></label></td>
        </tr>
    </tbody>
</table>
<hr />
<h3>Result:</h3>
<pre id="results"></pre>

@section Scripts {
    <script src="~/js/oidc-client.js"></script>
    <script type="module" src="~/js/app-config.js"></script>
    <script type="module" src="~/js/app.js"></script>
}

 

 

The page will be like following, there are three buttons that we will implement their callbacks.

·       Sign In

·       Sign Out

·       Test secured API

 

(Before logged in)


 

 

(Logged in)


 

 

 

app-config.js

 

We will set the client and identity server’s host URLs that will be used for redirect URLs after logged in or out in app.js.


export const AUTH_HOST_URL = "https://localhost:6001";
export const CLIENT_HOST_URL = "https://localhost:5001"; 

 

 

 

app.js

 

Set the OIDC configuration and logging,

import * as constants from './app-config.js';

function log() {
    document.getElementById('results').innerText = '';

    Array.prototype.forEach.call(argumentsfunction (msg) {
        if (msg instanceof Error) {
            msg = "Error: " + msg.message;
        }
        else if (typeof msg !== 'string') {
            msg = JSON.stringify(msgnull2);
        }
        document.getElementById('results').innerHTML += msg + '\r\n';
    });
}

var config = {
    authority: constants.AUTH_HOST_URL,
    client_id: "PkceJS",
    redirect_uri: `${constants.CLIENT_HOST_URL}/OpenId/Login/JS`,
    response_type: "code",
    scope: "openid profile offline_access MyBackendApi2",
    post_logout_redirect_uri: `${constants.CLIENT_HOST_URL}/OpenId/Login/JS`
};
 

 

 

Implement the callbacks of Sign In, Sign Out and Test secured API.


document.getElementById("welcome_msg").hidden = true;
document.getElementById("login").addEventListener("click"loginfalse);
document.getElementById("api").addEventListener("click"apifalse);
document.getElementById("logout").addEventListener("click"logoutfalse);

var mgr = new Oidc.UserManager(config);

mgr.signinRedirectCallback().then(function (user) {
    if (user) {
        document.getElementById("signin_msg").hidden = true;
        document.getElementById("welcome_msg").hidden = false;
        document.getElementById("uid").innerText = user.profile.sub;
        document.getElementById("id_token").innerText = user.id_token;
        document.getElementById("access_token").innerText = user.access_token;
        document.getElementById("refresh_token").innerText = user.refresh_token;
        document.getElementById("expires_at").innerText = moment.unix(user.expires_at).utc();
        log("User logged in"user);
    }
    else {
        log("User not logged in");
    }
});

function login() {
    mgr.signinRedirect();
}

function logout() {
    // Signout
    mgr.signoutRedirect();
}

function api() {
    mgr.getUser().then(function (user) {
        var url = `${constants.CLIENT_HOST_URL}/api/DemoPolicyBased/Admin/Get`// You can change the test API 

        var xhr = new XMLHttpRequest();
        xhr.open("GET"url);
        xhr.onload = function () {
            log(xhr.statusxhr.responseText);
        }

        xhr.setRequestHeader("Authorization""Bearer " + user.access_token);
        xhr.send();
    });
}

 

The full code of app.js is here.

 

 

  

Source Code

Github: KarateJB/AspNetCore.IdentityServer4.Sample

 

 

 

Reference


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

IdentityModel/oidc-client-js

IdentityServer4: Adding a JavaScript client

 

 

 

 

 

 

 

沒有留言:

張貼留言