Auth0をPasswordless認証機構として使う

メモ。

# ToDo

* Auth0のユーザAPIやToken inspectionを使わずに、 `email_verified` でEmailアドレスの取得を済ませたい気持ちがある
* LockはEmailアドレスの記憶までは面倒を見てくれないっぽい

... 書いておいてアレだけどソーシャルログインで十分だな。。

# 何ができるの？

Auth0 ( https://auth0.com ) は7000アカウントまで対応した無料プランを持っており、この無料プランには、いわゆるPasswordless認証機構が含まれている。

これを使うことで、アカウントを要求するWebサイトでよくある:

1. E-mailアドレスを入力させる
2. ユーザにE-mailが送られてくる
3. ユーザがE-mail中のリンクをクリックする
4. サイトにログイン

というフローを無料で実現できる。認証結果はOpenID Connectで言うところのID Tokenで提供されるため、ID TokenをWebアプリケーション側で検証してやる(& 必要に応じて専用のトークンを更に振り出す)必要はある。

# Auth0側でやること

1. Applicationを作る( https://manage.auth0.com/#/applications ) - これにより、Client IDやClient Secretが発行される。
2. (APIを作る( https://manage.auth0.com/#/apis ) - これにより、OpenID Connectで言うところのaudienceを作成できる。(Auth0上では Identifier と呼ばれる))
3. Hosted page( https://manage.auth0.com/#/login_page )をPasswordless用に書き換える (**重要**)
4. Connection から Passwordlessを有効にする

Auth0のログインページはLock( https://auth0.com/docs/libraries/lock/v11 )によって提供されているが、Passwordless用のものは別物になるため書き換えが必要になる。ポイントは:

- `var lock = new Auth0LockPasswordless(config...` のように `Auth0LockPasswordless` をインスタンシエートする
- `passwordlessMethod: 'link'` を `config` に加える

標準のログインページではPasswordlessは選択肢に表われず、Connectionsから有効にするだけでは使えないことになる。

```javascript
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <title>Sign In with Auth0</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>

  <!--[if IE 8]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/ie8/0.2.5/ie8.js"></script>
  <![endif]-->

  <!--[if lte IE 9]>
  <script src="https://cdn.auth0.com/js/base64.js"></script>
  <script src="https://cdn.auth0.com/js/es5-shim.min.js"></script>
  <![endif]-->

  <script src="https://cdn.auth0.com/js/lock/11.11/lock.min.js"></script>
  <script>
    // Decode utf8 characters properly
    var config = JSON.parse(decodeURIComponent(escape(window.atob('@@config@@'))));
    config.extraParams = config.extraParams || {};
    var connection = config.connection;
    var prompt = config.prompt;
    var languageDictionary;
    var language;
    
    if (config.dict && config.dict.signin && config.dict.signin.title) {
      languageDictionary = { title: config.dict.signin.title };
    } else if (typeof config.dict === 'string') {
      language = config.dict;
    }
    var loginHint = config.extraParams.login_hint;
    
    // Available Lock configuration options: https://auth0.com/docs/libraries/lock/v11/configuration
    var lock = new Auth0LockPasswordless(config.clientID, config.auth0Domain, {
      auth: {
        redirectUrl: config.callbackURL,
        responseType: (config.internalOptions || {}).response_type ||
          (config.callbackOnLocationHash ? 'token' : 'code'),
        params: config.internalOptions
      },
      /* additional configuration needed for custom domains
      configurationBaseUrl: config.clientConfigurationBaseUrl,
      overrides: {
        __tenant: config.auth0Tenant,
        __token_issuer: 'YOUR_CUSTOM_DOMAIN'
      }, */
      assetsUrl:  config.assetsUrl,
      allowedConnections: connection ? [connection] : null,
      rememberLastLogin: !prompt,
      language: language,
      languageDictionary: languageDictionary,
      theme: {
        //logo:            'YOUR LOGO HERE',
        //primaryColor:    'green'
      },
      prefill: loginHint ? { email: loginHint, username: loginHint } : null,
      closable: false,
      defaultADUsernameFromEmailPrefix: false,
      passwordlessMethod: 'link'
      // uncomment if you want small buttons for social providers
      // socialButtonStyle: 'small'
    });

    lock.show();
  </script>
</body>
</html>
```

# 提供されるToken

適当にscope = `openid profile` としてOAuth2すると、以下のようなTokenが払い出される。

## Access Token

```json
{
  "iss": "https://yuni.auth0.com/",
  "sub": "email|5c2e8f08e8fd56b4e565b117",
  "aud": [
    "https://stripe.local",
    "https://yuni.auth0.com/userinfo"
  ],
  "iat": 1546559613,
  "exp": 1546566813,
  "azp": "as6bZ2s623qL3ga6UI1LqxySAPXFwgwq",
  "scope": "openid profile"
}
```

Access Tokenにはe-mailアドレスそのものは含まれていない。 `profile` スコープを要求したうえで、ID Tokenを取得する必要がある。

## ID Token

```json
{
  "nickname": "mjt",
  "name": "mjt@cltn.org",
  "picture": "https://s.gravatar.com/avatar/5cbf298165e52d62a8e0fc0114ce9d48?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fmj.png",
  "updated_at": "2019-01-03T23:53:31.322Z",
  "iss": "https://yuni.auth0.com/",
  "sub": "email|5c2e8f08e8fd56b4e565b117",
  "aud": "as6bZ2s623qL3ga6UI1LqxySAPXFwgwq",
  "iat": 1546559613,
  "exp": 1546595613,
  "at_hash": "1JMASj-DhLGRRmtNIDPVtg",
  "nonce": "qwerty"
}
```

`name` がEmailアドレスに相当する ...って `email_verified` とかじゃダメなんだろうか。。(TODO)