「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」のシリーズものです。
OpenID Connectにログインしてトークンを取得する作業がたびたび発生し、そのたびにログインページを編集するのが面倒になってきたので、汎用的なページを作っておきます。
今までも作ってあったのですが、どこに置いたかすぐ忘れてしまっていたので、いつでも呼び出せるようにこの便利ページに追加しました。
ソースコード一式は以下のGitHubにあります。
poruruba/utilities
単に使うだけの場合は以下を参照してください。ユーティリティタブからOIDCを選択してください。
#機能説明
事前に、「 https://poruruba.github.io/utilities/oidc_redirect.html 」または、立ち上げたドメイン名+/oidc_redirect_html をOpenID ConnectサーバにリダイレクトURLとして登録しておきます。
【エンドポイント呼び出し】
<response_type=codeの場合>
OpenID ConnectのクライアントID、Authorizeエンドポイントを指定して「Authoroze」ボタンを押下すると、ログイン画面が表示されます。
ログインが成功すると、認可コードを取得することができます。
その後、Tokenエンドポイントとクライアントシークレットを指定して「Token」ボタンを押下すると、IDトークンやアクセストークン、リフレッシュトークンを取得することができます。
トークンはJWT形式なので、見やすくJSON形式にしておきました。
<response_type=tokenの場合>
OpenID ConnectのクライアントID、Authorizeエンドポイントを指定して「Authorize」ボタンを押下すると、ログイン画面が表示されます。
ログインが成功すると、IDトークンおよびアクセストークンを取得することができます。
【入力情報のLocalStorage保存】
上記の機能に加えて、AuthorizeやTokenエンドポイント呼び出しが成功したときに、LocalStorageにエンドポイントやクライアントID、クライアントシークレットを覚えておくようにしましたので、再入力する必要がないようにしました。
【画面遷移しないログイン】
以下の投稿にある処理を実装しています。
AWS Cognitoの画面遷移しないサインインページを作る
#ソースコード
const REDIRECT_URL = "oidc_redirect.html";
var decoder = new TextDecoder('utf-8');
var new_win;
export default {
mixins: [mixins_bootstrap],
template: `
<div>
<h2 class="modal-header">OpenID Connect</h2>
<label class="title">authorize_endpoint</label>
<div class="input-group">
<input type="text" class="form-control" v-model="oidc_authorize_endpoint">
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li v-for="(item, index) in oidc_authorize_endpoint_list">
<a class="dropdown-item" v-on:click="oidc_select_item('authorize_endpoint','select', index)">
<span v-on:click.stop="oidc_select_item('authorize_endpoint', 'delete', index)">× </span> {{item}}
</a>
</li>
</ul>
</div>
<label class="title">client_id</label>
<div class="input-group">
<input type="text" v-model="oidc_client_id" class="form-control">
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li v-for="(item, index) in oidc_client_id_list">
<a class="dropdown-item" v-on:click="oidc_select_item('client_id', 'select', index)">
<span v-on:click.stop="oidc_select_item('client_id', 'delete', index)">× </span> {{item}}
</a>
</li>
</ul>
</div>
<div class="row">
<label class="title col-auto">response_type</label>
<div class="col-auto">
<select class="form-select" v-model="oidc_response_type">
<option value="token">token</option>
<option value="code">code</option>
</select>
</div>
</div>
<label class="title">redirect_uri</label> {{oidc_redirect_uri}}<br>
<label class="title">scope</label> <input type="text" v-model="oidc_scope" class="form-control">
<label class="title">state</label> <input type="text" v-model="oidc_state" class="form-control">
<button class="btn btn-primary" v-on:click="oidc_authorize">Authorize</button>
<hr>
<label class="title">code</label> <input type="text" class="form-control" v-model="oidc_code">
<label class="title">token_endpoint</label>
<div class="input-group">
<input type="text" class="form-control" v-model="oidc_token_endpoint">
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li v-for="(item, index) in oidc_token_endpoint_list">
<a class="dropdown-item" v-on:click="oidc_select_item('token_endpoint', 'select', index)">
<span v-on:click.stop="oidc_select_item('token_endpoint', 'delete', index)">× </span> {{item}}
</a>
</li>
</ul>
</div>
<label class="title">client_secret</label>
<div class="input-group">
<input type="text" class="form-control" v-model="oidc_client_secret">
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li v-for="(item, index) in oidc_client_secret_list">
<a class="dropdown-item" v-on:click="oidc_select_item('client_secret', 'select', index)">
<span v-on:click.stop="oidc_select_item('client_secret', 'delete', index)">× </span> {{item}}
</a>
</li>
</ul>
</div>
<label class="title">grant_type</label> {{oidc_grant_type}}<br>
<button class="btn btn-primary" v-on:click="oidc_token">Token</button>
<hr>
<label class="title">expires_in</label> {{oidc_expires_in}}<br>
<collapse-panel v-if="oidc_idtoken" id="oidc_panel_idtoken" title="IDトークン" collapse="true">
<span slot="content">
<div class="card-body">
<textarea class="form-control" rows="10" readonly>{{oidc_idtoken}}</textarea>
</div>
<div class="card-footer">
<button class="btn btn-secondary" data-bs-toggle="collapse" href="#oidc_panel_idtoken">閉じる</button>
</div>
</span>
</collapse-panel>
<collapse-panel v-if="oidc_accesstoken" id="oidc_panel_accesstoken" title="アクセストークン" collapse="true">
<span slot="content">
<div class="card-body">
<textarea class="form-control" rows="10" readonly>{{oidc_accesstoken}}</textarea>
</div>
<div class="card-footer">
<button class="btn btn-secondary" data-bs-toggle="collapse" href="#oidc_panel_accesstoken">閉じる</button>
</div>
</span>
</collapse-panel>
<collapse-panel v-if="oidc_refreshtoken" id="oidc_panel_refreshtoken" title="リフレッシュトークン" collapse="true">
<span slot="content">
<div class="card-body">
<textarea class="form-control" rows="10" readonly>{{oidc_refreshtoken}}</textarea>
</div>
<div class="card-footer">
<button class="btn btn-secondary" data-bs-toggle="collapse" href="#oidc_panel_refreshtoken">閉じる</button>
</div>
</span>
</collapse-panel>
<label class="title">{{oidc_token_type}}</label>
<div class="row">
<div v-if="oidc_idtoken_header" class="col-3">
<label class="title">ヘッダ</label>
<textarea class="form-control" rows="20" readonly>{{oidc_idtoken_header}}</textarea>
</div>
<div v-if="oidc_idtoken_payload" class="col-9">
<label class="title">ペイロード</label>
<textarea class="form-control" rows="20" readonly>{{oidc_idtoken_payload}}</textarea>
</div>
</div>
</div>`,
data: function () {
return {
oidc_authorize_endpoint: "",
oidc_response_type: "code",
oidc_client_id: "",
oidc_redirect_uri: "",
oidc_scope: "openid profile email",
oidc_state: "",
oidc_code: "",
oidc_client_secret: "",
oidc_token_endpoint: "",
oidc_grant_type: "authorization_code",
oidc_idtoken: null,
oidc_accesstoken: null,
oidc_refreshtoken: null,
oidc_idtoken_header: "",
oidc_idtoken_payload: "",
oidc_expires_in: 0,
oidc_token_type: "",
oidc_authorize_endpoint_list: [],
oidc_token_endpoint_list: [],
oidc_client_id_list: [],
oidc_client_secret_list: [],
}
},
methods: {
/* OpenID Connect */
oidc_authorize: async function(){
var params = {
authorize_endpoint: this.oidc_authorize_endpoint,
redirect_uri: this.oidc_redirect_uri,
response_type: this.oidc_response_type,
state: this.oidc_state,
client_id: this.oidc_client_id,
scope: this.oidc_scope,
};
new_win = open(REDIRECT_URL + '?' + to_urlparam(params), null, 'width=450,height=750');
},
oidc_token: async function(){
var params = {
grant_type: this.oidc_grant_type,
client_id: this.oidc_client_id,
redirect_uri: this.oidc_redirect_uri,
code: this.oidc_code,
};
return do_post_urlencoded_basic(this.oidc_token_endpoint, params, this.oidc_client_id, this.oidc_client_secret )
.then(result =>{
console.log(result);
this.oidc_expires_in = result.expires_in;
this.oidc_idtoken = result.id_token;
this.oidc_accesstoken = result.access_token;
this.oidc_refreshtoken = result.refresh_token;
this.oidc_token_type = "IDトークン";
var jwt = this.oidc_idtoken.split('.');
var header = decoder.decode(base64url.decode(jwt[0].trim()));
var payload = decoder.decode(base64url.decode(jwt[1].trim()));
this.oidc_idtoken_header = JSON.stringify(JSON.parse(header), null, '\t');
this.oidc_idtoken_payload = JSON.stringify(JSON.parse(payload), null, '\t');
if( this.oidc_token_endpoint_list.indexOf(this.oidc_token_endpoint) < 0 ){
this.oidc_token_endpoint_list.push(this.oidc_token_endpoint);
localStorage.setItem("oidc_token_endpoint_list", JSON.stringify(this.oidc_token_endpoint_list));
}
if( this.oidc_client_secret_list.indexOf(this.oidc_client_secret) < 0 ){
this.oidc_client_secret_list.push(this.oidc_client_secret);
localStorage.setItem("oidc_client_secret_list", JSON.stringify(this.oidc_client_secret_list));
}
})
.catch(error =>{
console.error(error);
alert(error);
});
},
oidc_do_token: function(message){
if( this.oidc_state && this.oidc_state != message.state ){
alert("OIDC: state is mismatch");
return;
}
if(message.access_token){
this.oidc_expires_in = message.expires_in;
this.oidc_idtoken = message.id_token;
this.oidc_accesstoken = message.access_token;
this.oidc_refreshtoken = null;
var token;
if( this.oidc_idtoken ){
this.oidc_token_type = "IDトークン";
token = this.oidc_idtoken;
}else{
this.oidc_token_type = "アクセストークン";
token = this.oidc_accesstoken;
}
var jwt = token.split('.');
var header = decoder.decode(base64url.decode(jwt[0].trim()));
var payload = decoder.decode(base64url.decode(jwt[1].trim()));
this.oidc_idtoken_header = JSON.stringify(JSON.parse(header), null, '\t');
this.oidc_idtoken_payload = JSON.stringify(JSON.parse(payload), null, '\t');
}else{
this.oidc_code = message.code;
}
if( this.oidc_authorize_endpoint_list.indexOf(this.oidc_authorize_endpoint) < 0 ){
this.oidc_authorize_endpoint_list.push(this.oidc_authorize_endpoint);
localStorage.setItem("oidc_authorize_endpoint_list", JSON.stringify(this.oidc_authorize_endpoint_list));
}
if( this.oidc_client_id_list.indexOf(this.oidc_client_id) < 0 ){
this.oidc_client_id_list.push(this.oidc_client_id);
localStorage.setItem("oidc_client_id_list", JSON.stringify(this.oidc_client_id_list));
}
},
oidc_select_item: function(target, type, index){
switch(target){
case "authorize_endpoint":{
if( type == "select" ){
this.oidc_authorize_endpoint = this.oidc_authorize_endpoint_list[index];
}else if( type == "delete"){
this.oidc_authorize_endpoint_list.splice(index, 1);
localStorage.setItem("oidc_authorize_endpoint_list", JSON.stringify(this.oidc_authorize_endpoint_list));
}
break;
}
case "token_endpoint":{
if( type == "select" ){
this.oidc_token_endpoint = this.oidc_token_endpoint_list[index];
}else if( type == "delete"){
this.oidc_token_endpoint_list.splice(index, 1);
localStorage.setItem("oidc_token_endpoint_list", JSON.stringify(this.oidc_token_endpoint_list));
}
break;
}
case "client_id":{
if( type == "select" ){
this.oidc_client_id = this.oidc_client_id_list[index];
}else if( type == "delete"){
this.oidc_client_id_list.splice(index, 1);
localStorage.setItem("oidc_client_id_list", JSON.stringify(this.oidc_client_id_list));
}
break;
}
case "client_secret":{
if( type == "select" ){
this.oidc_client_secret = this.oidc_client_secret_list[index];
}else if( type == "delete"){
this.oidc_client_secret_list.splice(index, 1);
localStorage.setItem("oidc_client_secret_list", JSON.stringify(this.oidc_client_secret_list));
}
break;
}
}
},
},
mounted: function () {
var url = new URL("./", location);
this.oidc_redirect_uri = url.href + REDIRECT_URL;
var list = localStorage.getItem('oidc_authorize_endpoint_list');
if( list )
this.oidc_authorize_endpoint_list = JSON.parse(list);
list = localStorage.getItem('oidc_token_endpoint_list');
if( list )
this.oidc_token_endpoint_list = JSON.parse(list);
list = localStorage.getItem('oidc_client_id_list');
if( list )
this.oidc_client_id_list = JSON.parse(list);
list = localStorage.getItem('oidc_client_secret_list');
if( list )
this.oidc_client_secret_list = JSON.parse(list);
}
};
function to_urlparam(qs){
var params = new URLSearchParams();
for( var key in qs )
params.set(key, qs[key] );
return params.toString();
}
function do_post_urlencoded_basic(url, params, client_id, client_secret){
var data = new URLSearchParams();
for( var name in params )
data.append(name, params[name]);
var basic = 'Basic ' + btoa(client_id + ':' + client_secret);
const headers = new Headers( { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : basic } );
return fetch(url, {
method : 'POST',
body : data,
headers: headers
})
.then((response) => {
if( response.status != 200 )
throw 'status is not 200';
return response.json();
})
}
以上