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

 

 

 

 

 

 

 

2021年4月5日 星期一

建立框架 : 我的評鑑報告心得



故事從兩周前收到晉升候選人的通知說起;雖然有一些晉升和對高階主管報告的經驗,不過這次卻面臨到全新的挑戰。

1.  必須在不到兩週的時間內完成兩次評鑑報告(備註1)。

2.  相較於其他被評鑑的同仁,因為目前擔任產品研發,所以參與外部專案(公司主要收入來源)的貢獻是相對薄弱的。

 

這兩次報告,我決定把重點放在技術的亮點過去如何帶領團隊完成不可能的任務的故事為主。

 

 

Pitch Anything為什麼GoogleLinkedIn、波音、高通、迪士尼都找他合作?書中提到:

 

所有訊息會先經過鱷魚腦初步過濾,不危險,不新鮮或不刺激的資訊,在大腦這一層就會被拋棄。




(
圖片來源:博客來)


而在報告過程中,我們的框架(氣場、視角)支配權乃致勝關鍵:

1. 不論有無意識到,每個人都在使用各自的框架。

2. 每個社交場合都會讓不同的框架聚在一起。

3. 框架之間無法長時間共存,而會相互碰撞,直到某個框架勝出。

4. 勝出的框架會主導人際互動,能支配其他框架。 


 

對於以技術為主的評鑑委員,我的框架鎖定以產品微服務化領域驅動設計(Domain Driven Design)兩項主軸,來帶出團隊如何能在開發新產品的同時,兼顧到彈性及擴展性。 這兩個主軸是公司開發人員近年很想也很難掌握的兩項技術,鐵定能讓技術底子的評鑑委員專注幾分鐘。

(事實證明,技術工作者對於怎麼做微服務化或使用DDD比較有興趣,至於產品有沒有賣出去就不那麼在意了。)

 

而針對以專案和客戶為導向的管理層,我以一個沒有SAP經驗的專案經理,實現跨集團SAP升級及導入新模組專案的故事,透過建立吊胃口框架大獎框架Pitch Anything》)來引起對方的興趣並暗示:

 

我曾從 0 到完成一個跨集團的專案,我已經準備好去完成更大的任務了,我想知道,你們也是否也準備好了?

 

但在報告中引用其他公司的經驗是個險招,畢竟我說的這個故事和要升遷我的公司毫無關係。 所以我必須再疊加其他框架來吸引這些身經百戰的主管。


What's Your Problem 你問對問題了嗎?:重組問題框架、精準決策的創新解決工具,提到了重組框架:

 

改變看待問題的方式(重組問題框架),可以幫助你找出更好的解決方案。

(圖片來源:博客來)

 

 

這個框架很適合拿來說明我如何能藉由過往經驗解決現在或未來專案的問題。

在所有的專案中,不管是專案延期成本增加或是無法驗收等(老闆和團隊的)痛點中,要解決的核心問題其實不是專案鐵三角(成本,範疇或時間)。

 

 

而是"",人才是提出和解決問題的關鍵。 所以我提出了以下的問題解決能力來加強我的框架:

 

本人歷年來作為業務單位和技術團隊的橋樑,擅長與人溝通及合作來完成任務,無關乎專案的大小或種類。

 

至於評鑑結果,並不是重點。 在過程中思考了自己的價值和發展方向,練習了幾種框架的表達方式,才是我最大的收穫。

 

 

參考書籍:

 

ü  就是品牌

ü  為什麼GoogleLinkedIn、波音、高通、迪士尼都找他合作?:募資提案教父1週談成6千萬的快‧精‧準攻心術

ü  你問對問題了嗎?:重組問題框架、精準決策的創新解決工具

 

 

 

 

備註1.

第一關(技術評鑑),評審委員有部門主管、PM和技術顧問。

第二關(領導和溝通評鑑),報告對象是公司的BU heads

 

 

 


2021年3月1日 星期一

[PostgreSQL] Recursive CTE for hierarchy data

 

 PostgreSQL   CTE   Hierarchy

 

 

Problem


 

Assume that we have a table, Departments, which include hierarchy data like this,


 


 

and we would like to know the Agile dev team’s parent hierarchy, for example,

Agile dev team / Tiger department / IT

Here is a sample for using CTE(Common table expression) and RECURSIVE modifier to solve the problem.

 

 

Solution


 

Recursive CTE


WITH RECURSIVE cte_recursive AS (
        SELECT *
        FROM "Departments"
        WHERE "Id" = '001'
        UNION ALL
        SELECT t.*
        FROM "Departments" t
        INNER JOIN cte_recursive r ON t."Id" = r."Parent"
)
SELECT * FROM cte_recursive;


 



 

 

The recursive CTE results in:

 



 

 

 

Function (Optional)

 

We can create a function for advanced usage.


CREATE OR REPLACE FUNCTION public.find_hirearchy_departmemts(id text) RETURNS SETOF "Departments" AS 
$function$
BEGIN
    RETURN QUERY
    WITH RECURSIVE cte_recursive AS (
        SELECT *
        FROM "Departments"
        WHERE "Id" = id
        UNION ALL
        SELECT t.*
        FROM "Departments" t
        INNER JOIN cte_recursive r ON t."Id" = r."Parent"    
)
    SELECT * FROM cte_recursive;
END;
$functionLANGUAGE plpgsql;

 

 

 

Reference


WITH Queries (Common Table Expressions)

PostgreSQL CTE