Auth0与Singpass的整合

874 阅读7分钟

谈到国家数字身份,新加坡有很多话要说。新加坡在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启动授权流程,从后端加载statenonce 到页面。

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。

Singpass sequence diagram

在交换之后,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 ,要么签名,要么加密。

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签名作为回报。

Singpass component diagram

  1. 用户访问一个需要针对Auth0进行认证的应用程序
  2. Auth0检测到没有会话,并重定向到通用登录页面进行互动认证
  3. 用户从可用的登录选项中选择用Singpass登录
  4. UL页面内的NDI库启动与Singpass的连接,获取QR码,并显示它
  5. 用户打开Singpass应用程序并扫描QR码
  6. Singpass应用程序与Singpass授权服务器进行后向通信,并接受登录。
  7. Singpass使用UL页面上NDI SDK的前台通道将回调重定向至Auth0的/login/callback端点
  8. Auth0连接到代理/token端点以交换授权码。令牌端点验证客户凭证
  9. 代理端点创建private_jwt客户端断言并交换授权码,验证响应并将结果返回给Auth0
  10. Auth0对从Singpass返回的id_token 进行解码,并向应用程序发布自己的id_token ,完成登录流程。

我们开发了四个开源版本的代理端点;托管于Auth0AWS LambdaCloudflare Workers和vanillaExpress.js。在本文的其余部分,我们将重点介绍Auth0内的Singpass集成代理的配置。对于生产使用,我们建议在你的环境中托管代理端点。

部署Singpass集成代理

首先,创建一个SPA伴侣应用程序,将允许的回调URL设置为"https://your-custom-domain/login/callback"

然后前往扩展,点击 "创建扩展 "并输入GitHub URLgithub.com/auth0-exten…在安装过程中填入正确的值,如下所示。

Extension settings

值为。

  • Auth0自定义域名
    这是你的Auth0自定义域名。你需要在Auth0中启用一个自定义域名。
  • Auth0客户端ID
    client_id Auth0中同伴应用程序的ID
  • Auth0客户端秘密
    client_secret Auth0中同伴应用程序的秘密
  • Singpass环境
    被设置为仅有Staging。
  • Singpass客户ID
    client_id 由Singpass分配给你。
  • Singpass signing alg
    Always ES256
  • 赖一方的jwks端点
    这是你在Singpass入职注册时使用的JWKS端点。
  • 赖方私钥
    你在Singpass入职注册时使用的椭圆曲线私钥。如果代理端点在你自己的基础设施中运行,这不是必需的,这是生产的首选部署方法。
  • 赖方kid
    这是你在Singpass入职注册时使用的JWKS密钥ID。

安装后,你需要在认证 > 社交 > 创建连接 > 创建自定义下的Singpass连接。使用同伴应用程序的client_id和secret,并根据你的租户地区和节点版本填充URL,如这里所记录的。Github repo中也有获取用户资料的脚本模板。最终,在这个连接上启用PKCE

singpass social connection

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

singpass application connection

通用登录

接下来,配置 "通用登录 "来承载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 login selection

点击 "用Singpass登录"。二维码显示出来。singpass QR in lock

用Singpass移动应用程序扫描QR码。

app combined

用户资料

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

singpass user profile

在下一篇文章中,我们将启用Myinfo,在那里你将获取电子邮件和姓名信息,并使用它将Singpass用户与具有匹配详细信息的现有客户联系起来。请继续关注!

行动中的登录体验

注意:本视频是在暂存环境中使用暂存应用程序(v13.0.0-stg)拍摄的。实际的应用程序界面可能有所不同。

关于Auth0

Auth0身份认证平台是Okta的一个产品单元,采用现代的身份认证方法,使企业能够为任何用户提供对任何应用程序的安全访问。Auth0是一个高度可定制的平台,开发团队想要多简单就有多简单,需要多灵活就有多灵活。Auth0每月保障数十亿次的登录交易,提供便利、隐私和安全,使客户能够专注于创新。欲了解更多信息,请访问auth0.com。