5月に行われた 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 を、追加で選択しておきます。
そういえば、今回のリリースで、Developer Console の画面もちょっとだけ変わりました。
準備.2 : WebDriver 入手
今回も、WebDriver を使っていきます。
こちら を参考にしてください。
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
を取得しておきます。
$bootBoy = @{
capabilities = {}
}
$bootResponse = Invoke-RestMethod -Uri "http://localhost:9515/session" -Method POST -body (ConvertTo-Json $bootBoy)
$sessionId = $bootResponse.value.sessionId
2. Token 取得
まずは、OpenID Connect の構成情報を取得してみましょう。
$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 を作成します。
$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 はこんな形になります。
$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 その他が返ります。
$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…
取得できたパラメータの切り出しまでやってみます。
$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"]
切り出した情報はこうなります。
$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 から、ヘッダーとペイロードを取得し、エンコードして、中身を確認してみましょう。
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
取得したヘッダーとペイロードはこんな感じです。
$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 が一致している必要があります。
if (!($tokenPayloadObj.iss.Equals($config.issuer)))
{
# Not valid
}
aud claim Validation
ペイロードの aud claim と、アプリのクライアント id $clientId
が一致している必要があります。
if (!($tokenPayloadObj.aud.Equals($clientId)))
{
# Not Valid
}
nonce claim Validation
ペイロードの nonce claim と、認証を要求した際の nonce $nonce
が一致している必要があります。
if (!($tokenPayloadObj.nonce.Equals($nonce)))
{
# Not Valid
}
iat / exp claim Validation
現在の時刻が、iat と exp の間である必要があります。
$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 に対応する公開鍵を取得します。
$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…
ヘッダーとペイロードをハッシュ化し、署名情報が正しいか検証します。
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 に等しいか確認します。
$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 へアクセスできます。