效果演示
关于如何部署 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的文章 - 知乎 》。
登录成功:
Duende IdentityServer 后台日志:
配置
要集成 Epic Games 登录,首先,需要去 Epic Games Dev Portal 创建一个 Client。
先决条件
同意 Epic Games 账号服务协议,如果你不是组织的拥有者,可以去组织成员里找到拥有者,并让拥有者去同意相关的协议。
同意协议的链接是:<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);>
将域名添加到组织里
打开页面: https://dev.epicgames.com/portal/en-US/你的组织ID/organization/settings,添加域名,并且认证。
概念
要创建客户端,先来搞清楚一些 Epic Games 里的概念。主要需要了解一些关系:
每个客户端可以关联多个应用,由于现在我们要对接的是一个 Web 应用,所以可以为客户端设置一个 Web 应用即可。
创建产品
比如我们创建了一个名为 Account 的产品,Epic Games 会给我们创建沙盒,我们可以查看沙盒详情,如下:
创建客户端
我们在产品下创建客户端。你如果有多个环境,可以分别创建多个客户端,创建完成后,可以从这个链接里列出你创建的所有客户端: https://dev.epicgames.com/portal/en-US/你的组织ID/products/你的产品ID/epic-account-services
记下客户端凭据
一旦客户端创建完成,就会得到一组凭据,记下来。
添加回调地址
没有回调地址,在授权完成后,就接收不到 code。配置如下:
创建应用
我们在上面创建好了客户端,它是为后台通道调用(使用客户端凭据获取令牌)使用的。现在我们还需要为用户授权界面创建前端通道。在 Epic Games 这被称为应用。我们可以通过在 “Epic Account Services” 下面创建应用,并且在其中关联客户端。
关联客户端
填写法律必须的 URL
这依赖之前的域名验证步骤,只能基于验证通过后的域名,在后面添加路径。
- 应用站点: https://brickverse.net/
- 隐私协议页面: https://brickverse.net/
我这里没有填写真正的隐私协议页面,所以导致该应用不能通过 Epic Games 的审核。不过出于演示目的,不需要通过 Epic Games 的审核,已经可以使用了。
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://dev.epicgames.com/docs/web-api-ref/authentication?sessionInvalidated=true#requesting-an-access-token,通过这个令牌接口,我们提供上一步拿到的用户授权码加上我们的客户端凭据,就可以获取到用户的令牌了。需要注意的是在这一步,这个接口使用了 Basic Auth:
典型的响应
正常的响应如下:
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
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 的配置方式如下:
增加 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 = /;
});
}
}
}
完成
提交代码,上线测试,如开头所示。