效果演示

关于如何部署 Duende IdentityServer,请参考《<font style=color:rgb(18, 18, 18);>身份验证哪家强?IdentityServer 初体验 - Jeff Tian的文章 - 知乎 》和《<font style=color:rgb(18, 18, 18);>Free Arch: 将 IdentityServer 部署到 Okteto - Jeff Tian的文章 - 知乎 》。

1690369808872 ab75c680 f636 4a78 8d1c 421e3e6794f3

登录成功:

1690369991556 efe82876 d722 41b1 b53e c8642cd551d4

Duende IdentityServer 后台日志:

1690371025303 4a72c077 52c9 4d74 97f5 3420001d72c8

配置

要集成 Epic Games 登录,首先,需要去 Epic Games Dev Portal 创建一个 Client。

先决条件

同意 Epic Games 账号服务协议,如果你不是组织的拥有者,可以去组织成员里找到拥有者,并让拥有者去同意相关的协议。

1690370532920 70d6bbb7 a8c3 42a7 8931 646fe4474f1f

同意协议的链接是:<font style=color:rgb(23, 43, 77);> https://dev.epicgames.com/portal/en-US/你的组织ID/organization/licenses<font style=color:rgb(23, 43, 77);>.

<font style=color:rgb(23, 43, 77);>
1690370988147 4f3c4111 231b 4311 a13f ad6a458a4257

将域名添加到组织里

打开页面: https://dev.epicgames.com/portal/en-US/你的组织ID/organization/settings,添加域名,并且认证。

1690371188734 9d4ffdc4 8ba5 4bde bc66 09acbb7a1823

1690371224217 0535efec 3c08 40c2 b41d 52e84510ae7f

概念

要创建客户端,先来搞清楚一些 Epic Games 里的概念。主要需要了解一些关系:

1690371596725 c3cd78f0 6e6e 42db bcf8 6a8e5cf22202

每个客户端可以关联多个应用,由于现在我们要对接的是一个 Web 应用,所以可以为客户端设置一个 Web 应用即可。

创建产品

比如我们创建了一个名为 Account 的产品,Epic Games 会给我们创建沙盒,我们可以查看沙盒详情,如下:

1690371486524 69d964b0 3e6d 44e1 8798 8cacba711673

创建客户端

我们在产品下创建客户端。你如果有多个环境,可以分别创建多个客户端,创建完成后,可以从这个链接里列出你创建的所有客户端: https://dev.epicgames.com/portal/en-US/你的组织ID/products/你的产品ID/epic-account-services

记下客户端凭据

一旦客户端创建完成,就会得到一组凭据,记下来。

1690372241974 6d5ac3b1 2453 4cf3 b7b9 e4e04e2d8514

添加回调地址

没有回调地址,在授权完成后,就接收不到 code。配置如下: 1690372364345 a88bbbb2 0b31 4f9e b148 1c989be0fe29

创建应用

我们在上面创建好了客户端,它是为后台通道调用(使用客户端凭据获取令牌)使用的。现在我们还需要为用户授权界面创建前端通道。在 Epic Games 这被称为应用。我们可以通过在 “Epic Account Services” 下面创建应用,并且在其中关联客户端。

1690372447747 28d18004 1d52 4e52 a4bd 1b3b7da8a840

关联客户端

1690372489794 dd2c75ec f7ae 45ce 83b7 37e72799c4ce

填写法律必须的 URL

这依赖之前的域名验证步骤,只能基于验证通过后的域名,在后面添加路径。

1690372643773 23d8e894 219b 4ce9 9684 958ad26de0b9

我这里没有填写真正的隐私协议页面,所以导致该应用不能通过 Epic Games 的审核。不过出于演示目的,不需要通过 Epic Games 的审核,已经可以使用了。

1690372741821 b8fb6c6a 261b 48d5 a78d 27d9c7dde314

Epic Games 的 OIDC 端点

即使应用没有通过 Epic Games 的审核,已经可以调用它们的 OIDC 接口了(只是用户授权时会看到一个警告)!

<font style=color:rgb(23, 43, 77);>名称 <font style=color:rgb(23, 43, 77);>URL
<font style=color:rgb(23, 43, 77);>OpenID 自发现配置信息端点 https://api.epicgames.dev/epic/oauth/v1/.well-known/openid-configuration
<font style=color:rgb(23, 43, 77);>授权接口 https://www.epicgames.com/id/authorize
<font style=color:rgb(23, 43, 77);>令牌接口 https://api.epicgames.dev/epic/oauth/v1/token
<font style=color:rgb(23, 43, 77);>用户信息接口 https://api.epicgames.dev/epic/oauth/v1/userInfo
<font style=color:rgb(23, 43, 77);>令牌内省接口 https://api.epicgames.dev/epic/oauth/v1/tokenInfo
<font style=color:rgb(23, 43, 77);>JWKs https://api.epicgames.dev/epic/oauth/v1/.well-known/jwks.json

<font style=color:rgb(23, 43, 77);>授权

参考 https://dev.epicgames.com/docs/web-api-ref/authentication?sessionInvalidated=true#web-applications,这会打开一个页面,询问用户是否同意授权我们的应用获取其在 Epic Games 服务器上的信息。如果用户同意了,我们就会得到一个用户授权码。

https://www.epicgames.com/id/authorize?client_id=%7Bclient_id%7D&redirect_uri=%7Bredirect_uri%7D&response_type=code&scope=basic_profile

获取令牌

参考 https://dev.epicgames.com/docs/web-api-ref/authentication?sessionInvalidated=true#requesting-an-access-token,通过这个令牌接口,我们提供上一步拿到的用户授权码加上我们的客户端凭据,就可以获取到用户的令牌了。需要注意的是在这一步,这个接口使用了 Basic Auth:

1690373116404 c6f3159f 9d27 4fb1 8fd8 c557af106560

典型的响应

正常的响应如下:

1690373164944 e203a8fb da3d 4c90 99aa 56f285d0c611

cURL 示例如下:

shell curl --location https://api.epicgames.dev/epic/oauth/v1/token --header Content-Type: application/x-www-form-urlencoded --header Authorization: Basic [base64(appId+appSecret)] --data-urlencode grant_type=authorization_code --data-urlencode deployment_id=[deployment_id] --data-urlencode scope=basic_profile --data-urlencode code=95fe72821ac94e5f9c48d5dda9b42e5c

响应的 Json 结构是这样的:

json { scope: basic_profile openid, token_type: bearer, access_token: eyJ0IjoiZXBpY19pZCIsImFsZyI6IlJTMjU2Iiwia2lkIjoiV01TN0Vua0lHcGNIOURHWnN2MldjWTl4c3VGblpDdHhaamo0QWhiLV84RSJ9.eyJzdWIiOiI3NTk4MzU2ODQxMDE0OWJiYmY0YzllMDZmMjk1MjZiMCIsInBmc2lkIjoiMDM5MGFkNmU5NDY2NDI4MDliYTA1ODEyOGRlNzBkY2MiLCJpc3MiOiJodHRwczpcL1wvYXBpLmVwaWNnYW1lcy5kZXZcL2VwaWNcL29hdXRoXC92MSIsImRuIjoiSmVmZiBUaWFuIiwibm9uY2UiOiJpUEFnbkM1QlZHU0h4MkJrd200d3J3IiwicGZwaWQiOiJhOTA2NjkwYzBkOTg0NGUxOWQzMWRkODRiMTRiMjYxMiIsInNlYyI6MSwiYXVkIjoieHl6YTc4OTFVS0FOVk5TZzdrQkZTSWdWUTRKSUNIdTAiLCJwZmRpZCI6IjczZmY5Njk3NjI3OTQxYjNhNGQxOGRkN2Q3MGExYTFkIiwidCI6ImVwaWNfaWQiLCJzY29wZSI6ImJhc2ljX3Byb2ZpbGUgb3BlbmlkIiwiYXBwaWQiOiJmZ2hpNDU2N0V1ZnlEeVdjb1Z3bGdwWFd4dFlIZFFrZyIsImV4cCI6MTY4NTAyNTg4OCwiaWF0IjoxNjg1MDE4Njg4LCJqdGkiOiI1YWM3MDM1NjIyOTA0YzI3OWIwNjZhMDk2NzNlMzg0OSJ9.eW5Lv8dhNlXHa4YX_q9ivBXX-kl8nwmH8fY-u4hMnE9IFXFGlXzIfenH4VzQi4_Qg23kpkLZqH7lDB1VKrz2_c0_qtRe660FPUM5h6GXSI8eV4lfqbDZtHIjoTuog0jBmnPNSCM-gn6NCpfhxb8rs1HgS5sTTEyloRANDZEbI-Q4I4AcEe4HAp6Ar-v0pdNDIzOPgC5vsQuqRuxinecmN3UiuoVTEIJlykF6--F8KepD4nnV_gqmADz62NQ7MkTzS5qQV8njrAHglXWZ2MOVXPxtYs4qym9AbpsK_tNH9zu7sM1YH4dPbTRDUrBt8DbuTuK-qbJmBn841Ua0g165Fg, refresh_token: eyJ0IjoiZXBpY19pZCIsImFsZyI6IlJTMjU2Iiwia2lkIjoiV01TN0Vua0lHcGNIOURHWnN2MldjWTl4c3VGblpDdHhaamo0QWhiLV84RSJ9.eyJzdWIiOiI3NTk4MzU2ODQxMDE0OWJiYmY0YzllMDZmMjk1MjZiMCIsInBmc2lkIjoiMDM5MGFkNmU5NDY2NDI4MDliYTA1ODEyOGRlNzBkY2MiLCJpc3MiOiJodHRwczpcL1wvYXBpLmVwaWNnYW1lcy5kZXZcL2VwaWNcL29hdXRoXC92MSIsImRuIjoiSmVmZiBUaWFuIiwicGZwaWQiOiJhOTA2NjkwYzBkOTg0NGUxOWQzMWRkODRiMTRiMjYxMiIsImF1ZCI6Inh5emE3ODkxVUtBTlZOU2c3a0JGU0lnVlE0SklDSHUwIiwicGZkaWQiOiI3M2ZmOTY5NzYyNzk0MWIzYTRkMThkZDdkNzBhMWExZCIsInQiOiJlcGljX2lkIiwiYXBwaWQiOiJmZ2hpNDU2N0V1ZnlEeVdjb1Z3bGdwWFd4dFlIZFFrZyIsInNjb3BlIjoiYmFzaWNfcHJvZmlsZSBvcGVuaWQiLCJleHAiOjE2ODUwNDc0ODgsImlhdCI6MTY4NTAxODY4OCwianRpIjoiODMxNDYzZTU5NDkwNDA4OTk3OGVjN2RiYWUyYzYzOTUifQ.Tf1QIEvo9GeqWlMK18ccEpRK24tNpi00EEVo_3cEUT-qIxtTK7w1LXQ6WG7jijKzPLz71SA98VgPACNCU8AgzbIXh5RbJ4RDAZSz63PMmhMyZW0PEEhMj4wPWii6yCWI6jkWsJYreZj9DNw_ChVd3wkjFHw62ceQ-EieT_VxMn0hCkwX1WmpMxkDsQ687aadk6Qmccl5Bqqwl2OmRsr8HgKKJNCdRXIXF5xM_5bdXNknTWRDCaCOyOcxuxAwxyK0cMTF1oLdMGc7CydIYJjGaePRb-jXbqJPaNk28ecMlmHN4i7yrsm4lb-WEDLUtmzZ7C3Jp6CJMDFQ6FPoy-PbSw, id_token: eyJ0IjoiaWRfdG9rZW4iLCJhbGciOiJSUzI1NiIsImtpZCI6IldNUzdFbmtJR3BjSDlER1pzdjJXY1k5eHN1Rm5aQ3R4WmpqNEFoYi1fOEUifQ.eyJzdWIiOiI3NTk4MzU2ODQxMDE0OWJiYmY0YzllMDZmMjk1MjZiMCIsInBmc2lkIjoiMDM5MGFkNmU5NDY2NDI4MDliYTA1ODEyOGRlNzBkY2MiLCJpc3MiOiJodHRwczpcL1wvYXBpLmVwaWNnYW1lcy5kZXZcL2VwaWNcL29hdXRoXC92MSIsImRuIjoiSmVmZiBUaWFuIiwibm9uY2UiOiJpUEFnbkM1QlZHU0h4MkJrd200d3J3IiwicGZwaWQiOiJhOTA2NjkwYzBkOTg0NGUxOWQzMWRkODRiMTRiMjYxMiIsImF1ZCI6Inh5emE3ODkxVUtBTlZOU2c3a0JGU0lnVlE0SklDSHUwIiwicGZkaWQiOiI3M2ZmOTY5NzYyNzk0MWIzYTRkMThkZDdkNzBhMWExZCIsInQiOiJpZF90b2tlbiIsImFwcGlkIjoiZmdoaTQ1NjdFdWZ5RHlXY29Wd2xncFhXeHRZSGRRa2ciLCJleHAiOjE2ODUwMjU4ODgsImlhdCI6MTY4NTAxODY4OCwianRpIjoiZjllNmFhM2UzOGMyNDg3ZGJiNDE2NDhiYzc5MDZkYTQifQ.MqhRi3Rxn1kuGOpeFFtWMtUwbE8isNRp4dia4aVLVepjPFI1qera0PY3SUncRmRpRtbo7LVnc0H6dLTkIWlAo3SwjxbVAAiUaXaMOi2dfBklRJb2z8PRqqxUJScDMI0klYoMYhHAD9NphEwwMNwGW82-gD-2qhgDafDDXbPLeNVTfMj3Wih4S0_vXGGTihiMYL8DNS-OGDXSMD9DrwQEgzEv0WaRFzP8BHqlNS8bt70yzaTc1xzv25Mi6gUWZOboOcQs-t6kYSxpGoBVJA8_1DxfJ6EVUO2brjFigxRWSJ3eY2oji8h8mau9Fl33Tpzz7x58Plsys5Z-16a76soY2w, expires_in: 7200, expires_at: 2023-05-25T14:44:48.000Z, refresh_expires_in: 28800, refresh_expires_at: 2023-05-25T20:44:48.005Z, account_id: 75983568410149bbbf4c9e06f29526b0, client_id: xyza7891UKANVNSg7kBFSIgVQ4JICHu0, application_id: fghi4567EufyDyWcoVwlgpXWxtYHdQkg, selected_account_id: 75983568410149bbbf4c9e06f29526b0, merged_accounts: [] }

代码实现

主要提交见:https://github.com/Jeff-Tian/IdentityServer/commit/41233386812e598c0a9050ad2b187c51fb6cf9e4

增加 Epic Games 配置

我们的客户端凭据信息等,可以通过环境变量注入。为了结构化地读取这些外部身份提供商的配置,我们定义一个配置类:

csharp namespace IdentityServerHost.Configuration;

public class ExternalIdPsConfiguration { public const string SectionName = ExternalIdPs;

public EpicGamesConfiguration EpicGames { get; set; } = new();

}

public class ExternalIdPConfiguration { public string ClientId { get; set; } = string.Empty; public string ClientSecret { get; set; } = string.Empty; }

public class EpicGamesConfiguration : ExternalIdPConfiguration { public const string SectionName = EpicGames; }

然后,定义一个配置扩展类,以封装读取配置的逻辑:

csharp using IdentityServerHost.Configuration; using Microsoft.Extensions.Configuration;

namespace IdentityServerHost.Extensions;

public static class ConfigurationExtensions { public static ExternalIdPsConfiguration GetExternalIdPsConfiguration(this IConfiguration configuration) { return configuration.Get(ExternalIdPsConfiguration.SectionName); }

public static CookieConfiguration GetCookieConfiguration(this IConfiguration configuration)
{
    return configuration.Get<CookieConfiguration>(CookieConfiguration.SectionName);
}
private static T Get<T>(this IConfiguration configuration, string section)
{
    return configuration.GetSection(section).Get<T>();
}

}

环境变量和 appsettings.json 结构有一种对应关系。我们定义的类,在 appsettings.json 文件里是这样的:

json { ExternalIdPs: { EpicGames: { ClientId: xyza7891UKANVNSg7kBFSIgVQ4JICHu0, ClientSecret: xxx } }, }

通过 k8s secret 的环境变量注入的方式如下:

yaml apiVersion: v1 kind: Secret metadata: name: id6 labels: branch: main type: Opaque stringData: ExternalIdPs__EpicGames__ClientId: xyza7891UKANVNSg7kBFSIgVQ4JICHu0 ExternalIdPs__EpicGames__ClientSecret: xxx

通过 Azure Portal 的配置方式如下:

1690374050107 3972b8d8 b4e3 471c 86bd ee911fb40091

增加 Epic Games 登录

在 hosts/main/Extensions/ExternalIdentityProviders.cs里添加一个新的方法:

csharp using System.Threading.Tasks; using Duende.IdentityServer; using IdentityModel.Client; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection;

namespace IdentityServerHost.Extensions;

public static class ExternalIdentityProviders { public static void AddGitHubProvider(this WebApplicationBuilder builder) { // ... }

public static void AddEpicGamesProvider(this WebApplicationBuilder builder)
{
    var externalIdPsConfiguration = builder.Configuration.GetExternalIdPsConfiguration();
    var epicGamesProviderConfig = externalIdPsConfiguration?.EpicGames;
    

    if (epicGamesProviderConfig != null && epicGamesProviderConfig.ClientSecret != string.Empty)
    {
        builder.Services.AddAuthentication().AddOpenIdConnect(epic-games, Epic Games, options =>
        {
            options.Scope.Add(basic_profile);
            options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
            options.Events.OnRemoteFailure = context =>
            {
                context.HandleResponse();
                return Task.CompletedTask;
            };
            options.Events.OnAccessDenied = context =>
            {
                context.HandleResponse();
                return Task.CompletedTask;
            };
            options.ClientId = epicGamesProviderConfig.ClientId;
            options.ClientSecret = epicGamesProviderConfig.ClientSecret;
            options.ResponseType = code;
            options.Authority = https://api.epicgames.dev/epic/oauth/v1;
            options.Events.OnAuthorizationCodeReceived = context =>
            {
                context.Backchannel.SetBasicAuthentication(context.TokenEndpointRequest!.ClientId,
                    context.TokenEndpointRequest.ClientSecret);
                
                context.TokenEndpointRequest.ClientId = null;
                context.TokenEndpointRequest.ClientSecret = null;
                return Task.CompletedTask;
            };
            options.CorrelationCookie.Path = /;
            
        });
    }
}

}

完成

提交代码,上线测试,如开头所示。