LINE WORKS で OpenID Connect / Implicit Flow を試す

５月に行われた LINE WORKS Developers のアップデートで、新たに OpenID Connect （OIDC） に対応しました。
OIDC では、ID 情報を含む ID Token を取得することが出来ます。
さらに、OIDC 利用時には、これまで OAuth で利用できた Authorzation Code Flow に加えて、Implicit Flow も利用できるようになりました。

今回は、Implicit Flow を使った OpenID Connect で、ID Token を取得してみます。

## 準備.1 : Scope の設定
OIDC を使用するには、アプリの **OAuth Scopes** で、**openid** scope を選択しておく必要があります。ID Token にメール情報も含めたい場合には **email** scope を、プロフィール情報も含めたい場合には **profile** scope を、追加で選択しておきます。

![スクリーンショット 2024-05-31 201316.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2667330/a19bd3b4-93a7-438e-a05e-02a4b7428340.png)


そういえば、今回のリリースで、Developer Console の画面もちょっとだけ変わりました。

## 準備.2 : WebDriver 入手
今回も、WebDriver を使っていきます。
[こちら](https://qiita.com/msato-3/items/37508eb84f518cf86b2a#%E6%BA%96%E5%82%992--webdriver-%E3%81%AE%E5%85%A5%E6%89%8B) を参考にしてください。




## 1. WebDriver 起動
まずは WebDriver を起動します。

``` 
Starting Microsoft Edge WebDriver 127.0.2599.0 (c36d7542654f6489f48a0bed1a58b3f2d2f89c14) on port 9515
To submit feedback, report a bug, or suggest new features, please visit https://github.com/MicrosoftEdge/EdgeWebDriver

Only local connections are allowed.
Please see https://aka.ms/WebDriverSecurity for suggestions on keeping Microsoft Edge WebDriver safe.

Microsoft Edge WebDriver was started successfully.
```

その後、WebDriver から Edge を起動し、コントロールするための `sessionId` を取得しておきます。
``` powershell 
$bootBoy = @{
    capabilities = {}
}
$bootResponse = Invoke-RestMethod -Uri "http://localhost:9515/session" -Method POST -body (ConvertTo-Json $bootBoy)
$sessionId    = $bootResponse.value.sessionId

```

## 2.  Token 取得
まずは、OpenID Connect の構成情報を取得してみましょう。
``` powershell
$tenantId = '11111111'
$config   = Invoke-RestMethod -Method GET -Uri https://auth.worksmobile.com/$tenantId/.well-known/openid-configuration
$config | convertto-json
{                                                            
  "issuer": "https://auth.worksmobile.com",
  "authorization_endpoint": "https://auth.worksmobile.com/oauth2/v2.0/authorize",
  "token_endpoint": "https://auth.worksmobile.com/oauth2/v2.0/token",
  "revocation_endpoint": "https://auth.worksmobile.com/oauth2/v2.0/revoke",
  "jwks_uri": "https://auth.worksmobile.com/oauth2/v2.0/certs/11111111",
  "scopes_supported": [
    "openid",
    "email",
    "profile"
  ],
  "response_types_supported": [
    "code",
    "id_token",
    "token id_token"
  ],
  "grant_types_supported": [
    "authorization_code",
    "implicit",
    "refresh_token"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post"
  ],
  "claims_supported": [
    "iss",
    "aud",
    "sub",
    "iat",
    "exp",
    "email",
    "email_verified",
    "family_name",
    "given_name",
    "name",
    "locale"
  ]
}

```

**authorization_endpoint** に、アプリの情報をもとにしてクエリパラメータを追加して、アクセス URL を作成します。
``` powershell
$redirectUrl  = "https://localhost"
$clientId     = "audaudaud"
$scope        = "openid email profile directory"
$state        = "state"
$responseType = "token id_token"

$b = new-object byte[](24)
(new-object Random).NextBytes($b)
$nonce = [Convert]::ToBase64String($b).Replace('+','-').Replace('/','_')

$url = $config.authorization_endpoint + '?' +
    "client_id="      + [Uri]::EscapeDataString($clientId) +
    "&redirect_uri="  + [Uri]::EscapeDataString($redirectUrl) +
    "&scope="         + [Uri]::EscapeDataString($scope) +
    "&state="         + [Uri]::EscapeDataString($state) +
    "&nonce="         + $nonce +
    "&response_type=" + [Uri]::EscapeDataString($responseType)
```

URL はこんな形になります。
```powershell
$url
https://auth.worksmobile.com/oauth2/v2.0/authorize?client_id=audaudaud&redirect_uri=https%3A%2F%2Flocalhost&scope=openid%20email%20profile%20directory&state=state&nonce=nczd5ft3iycWEsyPYFJuNJOnhAnUQ0KD&response_type=token%20id_token
```

WebDriver からこの URL にアクセスします。認証画面が表示されますので、ログイン情報を入力してユーザー認証をします。
成功すれば、指定した Redirect URL にリダイレクトされ、URL フラグメントに Token その他が返ります。
```powershell
$authReuestbody = @{
    url = $url
}
Invoke-RestMethod -Uri "http://localhost:9515/session/$sessionId/url" -Method POST -body (ConvertTo-Json $authReuestbody) 

do{
    start-sleep -Seconds 1
    $res = Invoke-RestMethod -Uri "http://localhost:9515/session/$sessionId/url" -Method GET
}while ($false -eq $res.value.startswith($redirectUrl))
```

参考までに、こんな感じ。
```
 $res.value
https://localhost/#access_token=jp1AAAA4fw35xWV5T8bZyXSaQ78Py/I5fIGPC1lzmyJGN…

```

取得できたパラメータの切り出しまでやってみます。
```PowerShell
$params = [Uri]::UnescapeDataString($res.value.Split('#')[1])
$paramsCollection = @{}
$paramsArray = $params.Split('&')
foreach($record in $paramsArray)
{
    $idx = $record.IndexOf('=')
    $paramsCollection.add( $record.substring(0,$idx), $record.substring($idx+1))
}

$accessToken_in_response = $paramsCollection["access_token"]
$id_token_in_response    = $paramsCollection["id_token"]
$state_in_response       = $paramsCollection["state"]
$token_type_in_response  = $paramsCollection["token_type"]
$expires_in_in_response  = $paramsCollection["expires_in"]
$scope_in_response       = $paramsCollection["scope"]
```

切り出した情報はこうなります。
```PowerShell
$paramsCollection

Name           Value
----           -----
expires_in     86400
access_token   jp1AAAA4fw35xWV5T8bZyXSaQ78Py/I5fIGPC1lzmyJGNn6…
scope          openid email profile directory
token_type     Bearer
state          state
id_token       eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imp…

```


## ID Token の Validation
ID Token は、ヘッダー、ペイロード、署名からなる、JSON Web Signature (JWS) で署名された JSON Web Token (JWT) です。
ユーザー情報はペイロードに含まれており簡単に参照することはできますが、取得した ID Token は、Validation をしてから利用する必要があります。

まずは、ID Token から、ヘッダーとペイロードを取得し、エンコードして、中身を確認してみましょう。
```powershell
function getBytesFromBase64url ([String]$base64url)
{
    $str = $base64url.Replace('-','+').Replace('_','/');
    $mod = $str.Length % 4;
    if ($mod -ne 0){
        $str = $str.padRight($str.Length + (4-$mod), "=")
    }
    return [Convert]::FromBase64String($str)
}

function getStrFromBase64url ([String]$base64url)
{
    $bin = getBytesFromBase64url($base64url);
    return [System.Text.Encoding]::UTF8.GetString( $bin );
}

$token        = $id_token_in_response.split(".")
$tokenHeader  = $token[0]
$tokenPayload = $token[1]
$tokenDigKey  = $token[2]

$tokenHeaderObj  = getStrFromBase64url($tokenHeader) | ConvertFrom-Json
$tokenPayloadObj = getStrFromBase64url($tokenPayload) | ConvertFrom-Json
```

取得したヘッダーとペイロードはこんな感じです。
```Powershell
$tokenHeaderObj | ConvertTo-Json

{           
  "typ": "JWT",
  "alg": "RS256",
  "kid": "jm64WO526dV5xbL997kleldfljkreodlkjd"
}


$tokenPayloadObj | ConvertTo-Json
{
  "iss": "https://auth.worksmobile.com",
  "sub": "12345678-1234-1234-1234-1234567890ab",
  "aud": "audaudaud",
  "nonce": "nczd5ft3iycWEsyPYFJuNJOnhAnUQ0KD",
  "email": "midori@example.com",
  "name": "枠須みどり",
  "locale": "ja_JP",
  "exp": 1716431137,
  "iat": 1716427537,
  "at_hash": "_aJ7UPKs65m_87U__Uk9Jg",
  "email_verified": "true",
  "family_name": "枠須",
  "given_name": "みどり"
}

```

### iss claim Validation
ペイロードの *+iss** claim と、openid-configration の issuer が一致している必要があります。

```powershell
if (!($tokenPayloadObj.iss.Equals($config.issuer)))
{
    # Not valid 
}

```
### aud claim Validation
ペイロードの **aud** claim と、アプリのクライアント id `$clientId` が一致している必要があります。
```powershell
if (!($tokenPayloadObj.aud.Equals($clientId)))
{
    # Not Valid
}
```

### nonce claim Validation
ペイロードの **nonce** claim と、認証を要求した際の **nonce** `$nonce` が一致している必要があります。
```powershell
if (!($tokenPayloadObj.nonce.Equals($nonce)))
{
    # Not Valid
}
```

### iat / exp claim Validation
現在の時刻が、**iat** と **exp** の間である必要があります。
```powershell
$now = Get-Date
$currentUnixTime = [int](Get-Date($now) -UFormat '%s')
if (($tokenPayloadObj.iat -ge $currentUnixTime) -or ($tokenPayloadObj.exp -le $currentUnixTime))
{
    # Not Valid
}
```

### signature Validation
openid-configration の jwks_uri で公開されている鍵情報から、ID Token のヘッダ情報の **kid** に一致する公開鍵を参照し、ヘッダとペイロードを署名して、ID Token の署名に一致するか確認します。

まずは、ヘッダ情報の kid に対応する公開鍵を取得します。
```powershell
$jwks = Invoke-RestMethod -Method GET -Uri $config.jwks_uri 
$key = $jwks.keys | Where-Object{$_.kid -ceq $tokenHeaderObj.kid }
$key
kty : RSA
e   : AQAB
use : sig
kid : jm64WO526dV5xbL997kleldfljkreodlkjd
alg : RS256
n   : jlP8fgD0yyPmF8z6OvXRBXAWu579EIqJZPZHNLZv…
      
```
ヘッダーとペイロードをハッシュ化し、署名情報が正しいか検証します。
```powershell
using namespace System.Security.Cryptography
$publicKeyParams          = new-object RSAParameters
$publicKeyParams.Modulus  = getBytesFromBase64url($Key.n)
$publicKeyParams.Exponent = getBytesFromBase64url($key.e)
$publicKey                = new-object RSACryptoServiceProvider
$publicKey.ImportParameters($publicKeyParams)
$rsaDeformatter           = new-object RSAPKCS1SignatureDeformatter($publicKey)
$rsaDeformatter.SetHashAlgorithm("SHA256")

$sha256     = [System.Security.Cryptography.SHA256]::Create();
$hash       = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($tokenHeader + "." + $tokenPayload))

$signBinary = getBytesFromBase64url($tokenDigKey)

$isValid = $rsaDeformatter.VerifySignature($hash, $signBinary)

if (!$isValid){
    # Not Valid
}
```

Validation が済んだ ID Token のペイロード内の情報を参照・利用します。


## Access Token Validation
Access Token も、Validation してから利用します。
まず、Access Token を、ID Token ヘッダの **alg** で指定されたハッシュアルゴリズムでハッシュ化します。
取得したハッシュ値の左側半分を Base64URL Encode し、ID Token ペイロードの **at_hash** claim に等しいか確認します。

```powershell
$atHash      = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($accessToken_in_response));
$half_atHash = $atHash[0 .. (($atHash.Length/2)-1)]
$half_atHash_base64url = [Convert]::ToBase64String($half_atHash).Replace('+','-').Replace('/','_').Replace('=','')

if (!($half_atHash_base64url -ceq $tokenPayloadObj.at_hash))
{
    # Not Valid
}
```

Validation が済んだ Access Token を使用して、Open API へアクセスできます。