谈到国家数字身份,新加坡有很多话要说。新加坡在2003年推出了Singpass,并且多年来一直在加强它。新加坡通票的最初目标是向政府机构验证公民身份,后来扩展到私营部门使用,相当成功。今天,新加坡居民使用新加坡通票安全地访问460多个政府机构和私营部门组织的1,700多项服务。
随着Auth0的客户群在东南亚、新加坡和日本迅速增长,是时候在Auth0平台上正式提供Singpass服务了。
这是两篇系列文章中的第一篇。在这篇文章中,我们将学习如何将Singpass QR登录与Auth0通用登录整合。在下一篇文章中,我们将研究如何整合Myinfo,以分享选择性的个人数据,如姓名和电子邮件地址。
Singpass QR登录如何工作
Singpass QR登录使用OIDC授权码流程。Singpass提供了一个嵌入式SDK,用于渲染从授权端点返回的QR码。
<script src=”https://id.singpass.gov.sg/static/ndi_embedded_auth.js”></script>
网站通过托管NDI嵌入式Javascript SDK与Singpass整合。在页面加载时,Singpass SDK启动授权流程,从后端加载state 和nonce 到页面。
const authParamsSupplier = async () => {
return { state: "my-state", nonce: "my-nonce"};
};
const onError = (errorId, message) => {
console.log(`Error. errorId:${errorId} message:${message}`);
};
const initAuthSessionResponse = window.NDI.initAuthSession(
'ndi-qr',
{
clientId: SINGPASS_CLIENT_ID,
redirectUri: SINGPASS_REDIRECT_URL,
scope: 'openid',
responseType: 'code'
},
authParamsSupplier,
onError
);
NDI客户端调用授权端点以返回一个嵌入式QR码。然后,Singpass应用程序扫描生成的QR码,用户随后确认登录。接下来,嵌入式SDK通过Websocket接收确认,并将授权码和状态重定向到客户端的回调URL。

在交换之后,id_token'ssub 索赔包含一个不透明的数字标识符,称为UUID或UUID和NRIC的组合。
{
"sub" : "s=S8829314B,u=1c0cee38-3a8f-4f8a-83bc-7a0e4c59d6a9",
"aud" : "xxNsTfleQMHoW6tbUgSVNwnLWQ0xTeV0",
"iss" : "https://stg-id.singpass.gov.sg",
"exp" : 1609907975,
"iat" : 1609907375,
"nonce" : "alh5DS2Gfndv9i0jXYViqGIhiQdP4+4BrUvBhDXBYKk=",
"amr" : [ "pwd", "swk" ]
}
根据所要求的信息,id_token ,要么签名,要么加密。

将Singpass与Auth0整合
在探索阶段,我们注意到Auth0提供的与外部OIDC供应商的整合与Singpass的要求之间存在一些差距。
第一个挑战是,应用程序发送至Auth0的nonce 参数在我们想要启动NDI SDK的UL内是不可用的。
其次,Auth0目前不支持OIDC连接的令牌端点的client-assertion。Singpass希望客户使用非对称client_assertion JWT,客户应遵循NDI的流程,在面向公众的文件中生成一个椭圆曲线密钥对和主机公钥。 jwks.json文件中。客户的公钥URL会在提交申请时与Singpass共享。
最后,Singpass的id_token 签名(id_token_signing_alg_values_supported ),是 ES256正如他们的openid-configuration中所指出的。Auth0只支持 RS256来自上游连接的签名id_tokens 。
虽然将Singpass与Auth0整合并不是一个原生功能,但我们希望在未来建立一个更简单、更容易的解决方案。
技术细节
我们的现场团队决定挑战自己,利用Auth0可扩展性平台内的可用工具,为Auth0上的Singpass开发一个集成路径。
我们决定代理授权和令牌端点。代理版本增加了附加nonce、与client_assertion进行交换和验证签名等缺失的功能。 ES256签名作为回报。

- 用户访问一个需要针对Auth0进行认证的应用程序
- Auth0检测到没有会话,并重定向到通用登录页面进行互动认证
- 用户从可用的登录选项中选择用Singpass登录
- UL页面内的NDI库启动与Singpass的连接,获取QR码,并显示它
- 用户打开Singpass应用程序并扫描QR码
- Singpass应用程序与Singpass授权服务器进行后向通信,并接受登录。
- Singpass使用UL页面上NDI SDK的前台通道将回调重定向至Auth0的/login/callback端点
- Auth0连接到代理/token端点以交换授权码。令牌端点验证客户凭证
- 代理端点创建private_jwt客户端断言并交换授权码,验证响应并将结果返回给Auth0
- Auth0对从Singpass返回的
id_token进行解码,并向应用程序发布自己的id_token,完成登录流程。
我们开发了四个开源版本的代理端点;托管于Auth0、AWS Lambda、Cloudflare Workers和vanillaExpress.js。在本文的其余部分,我们将重点介绍Auth0内的Singpass集成代理的配置。对于生产使用,我们建议在你的环境中托管代理端点。
部署Singpass集成代理
首先,创建一个SPA伴侣应用程序,将允许的回调URL设置为"https://your-custom-domain/login/callback"。
然后前往扩展,点击 "创建扩展 "并输入GitHub URLgithub.com/auth0-exten…在安装过程中填入正确的值,如下所示。

值为。
- Auth0自定义域名
这是你的Auth0自定义域名。你需要在Auth0中启用一个自定义域名。 - Auth0客户端ID
client_idAuth0中同伴应用程序的ID。 - Auth0客户端秘密
client_secretAuth0中同伴应用程序的秘密。 - Singpass环境
被设置为仅有Staging。 - Singpass客户ID
client_id由Singpass分配给你。 - Singpass signing alg
AlwaysES256 - 依赖一方的jwks端点
这是你在Singpass入职注册时使用的JWKS端点。 - 依赖方私钥
你在Singpass入职注册时使用的椭圆曲线私钥。如果代理端点在你自己的基础设施中运行,这不是必需的,这是生产的首选部署方法。 - 依赖方kid
这是你在Singpass入职注册时使用的JWKS密钥ID。
安装后,你需要在认证 > 社交 > 创建连接 > 创建自定义下的Singpass连接。使用同伴应用程序的client_id和secret,并根据你的租户地区和节点版本填充URL,如这里所记录的。Github repo中也有获取用户资料的脚本模板。最终,在这个连接上启用PKCE。

然后在需要使用Singpass登录的应用程序中启用此连接。

通用登录
接下来,配置 "通用登录 "来承载NDI SDK,并在要求时呈现QR登录码。进入品牌建设 > 通用登录 > 登录。启用 "自定义登录页面 "并配置页面HTML。
<script src="https://cdn.auth0.com/js/lock/11.30/lock.min.js"></script>
<!-- ADDED for Singpass -->
<script src="//code.jquery.com/jquery-3.1.0.min.js"></script>
<script src="https://stg-id.singpass.gov.sg/static/ndi_embedded_auth.js"></script>
<!-- /ADDED -->
在锁配置的theme 对象下添加authButtons 。
var lock = new Auth0Lock(config.clientID, config.auth0Domain, {
// ...
theme: {
authButtons: {
"singpass": {
displayName: "Singpass",
primaryColor: "#cf0b15",
foregroundColor: "#FFFFFF",
icon: "https://app.singpass.gov.sg/apple-touch-icon.png"
}
}
}
});
呈现二维码的逻辑如下。
lock.once('signin ready', function () {
console.log('siginin ready');
if (config.extraParams.singpass) {
$(".auth0-lock.auth0-lock").removeProp("box-sizing");
var connectionConfig = initConnnectionConfig();
addEnterpriseConnections(connectionConfig);
init();
}
});
lock.show();
function initConnectionConfig() {
return {
general: {
backButton: '<span class="auth0-lock-back-button"><svg focusable="false" enable-background="new 0 0 24 24" version="1.0" viewBox="0 0 24 24" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <polyline fill="none" points="12.5,21 3.5,12 12.5,3 " stroke="#000000" stroke-miterlimit="10" stroke-width="2"></polyline> <line fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="2" x1="22" x2="3.5" y1="12" y2="12"></line> </svg></span>',
enterprise_panel: '<div id="ndi-qr" class="auth0-enterprise-button-content"></div>'
}
};
}
function addEnterpriseConnections(connectionConfig) {
var backButton = $(connectionConfig.general.backButton);
$('.auth0-lock-submit').hide();
backButton.appendTo('.auth0-lock-header');
backButton.on('click', function (e) {
e.preventDefault();
window.history.back();
});
var enterprisePanel = $(connectionConfig.general.enterprise_panel);
enterprisePanel.appendTo('.auth0-lock-body-content');
$('.auth0-lock-content').hide();
enterprisePanel.show();
}
function init() {
const authParamsSupplier = async () => {
// Replace the below with an `await`ed call to initiate an auth session on your backend
// which will generate state+nonce values, e.g
return { state: decodeURIComponent(config.extraParams.ndi_state), nonce: decodeURIComponent(config.extraParams.ndi_nonce) };
};
const onError = (errorId, message) => {
console.log(`onError. errorId:${errorId} message:${message}`);
};
const initAuthSessionResponse = window.NDI.initAuthSession(
'ndi-qr',
{
clientId: SINGPASS_CLIENT_ID, // Replace with your Singpass client ID
redirectUri: SINGPASS_AUTH0_CALLBACK, // Replace with your Auth0 custom domain
scope: 'openid',
responseType: 'code'
},
authParamsSupplier,
onError,
{
renderDownloadLink: false,
appLaunchUrl: '' // Replace with your iOS/Android App Link
},
);
console.log('initAuthSession: ', initAuthSessionResponse);
}
你可以在这里找到自定义通用登录页面的完整版本。
运行时的情况
你应该能够在登录页面上看到用新加坡通登录作为连接选项。

点击 "用Singpass登录"。二维码显示出来。
用Singpass移动应用程序扫描QR码。

用户资料
我们采用Singpass UUID来形成Auth0user_id 。该逻辑位于Singpass连接的Fetch User Profile脚本中。

在下一篇文章中,我们将启用Myinfo,在那里你将获取电子邮件和姓名信息,并使用它将Singpass用户与具有匹配详细信息的现有客户联系起来。请继续关注!
行动中的登录体验
注意:本视频是在暂存环境中使用暂存应用程序(v13.0.0-stg)拍摄的。实际的应用程序界面可能有所不同。
关于Auth0
Auth0身份认证平台是Okta的一个产品单元,采用现代的身份认证方法,使企业能够为任何用户提供对任何应用程序的安全访问。Auth0是一个高度可定制的平台,开发团队想要多简单就有多简单,需要多灵活就有多灵活。Auth0每月保障数十亿次的登录交易,提供便利、隐私和安全,使客户能够专注于创新。欲了解更多信息,请访问auth0.com。