Magic Leap 1 OAuth Windowを使い、Google OAuth 2.0 認証によるログイン~ユーザー情報の取得について
概要
Lumin SDK 0.24からOAuth Window機能が追加されました。
今回は、OAuth Windowの機能説明とOAuth Windowを使い、Google OAuth 2.0 認証を使ったログイン処理~ユーザー情報の取得の一連の処理について説明します。 (開発についての説明はUnity Editorを使用した説明となります。)
Magic Leap 1 の OAuth Windowについて
Magic Leap 1 の OAuth Windowは、Magic Leap 1 アプリケーションからOAuth認証のWeb画面にアクセスを提供する機能です。
OAuth Windowの処理フロー
OAuth Window はログイン機能へのアクセス、ログイン後に認可サーバーから返却された認可コード、scope、state(※1)をMagic Leap 1 アプリケーションで取得することができます。
この認可コードを使い、アクセストークンやリフレッシュトークンの取得を行い、取得したアクセストークンを使ってアカウントに紐づく情報にアクセスするわけですが、Magic Leap が提供するOAuth Windowには、そのような機能は提供されていません。
OAutn Windowは、認可コード、scope、stateの取得まで。
アクセストークンの取得~アクセスしたい情報の取得処理は、自前で実装する必要があります。
開発環境
以下はLumin SDK のバージョンに対し、サポートしているUnity Editorのバージョンが全て網羅したページになります。
https://developer.magicleap.com/en-us/learn/guides/unity-doc-archive
当機能は、Unity、Unreal Engine、C API、Lumin Runtime、MagicScript をサポートしています。この記事では、Unityによる開発方法の説明となります。
事前準備
今回は、Magic Leap 1を使用し、GoogleアカウントのログインからGoogle アカウントの情報を取得を行います。
1. プロジェクトの作成
Google Developer Console(以下)にアクセスし、Googleアカウントのユーザー情報を取得するプロジェクトを作成します。
2.認証情報(OAuth 2.0 クライアント ID)の作成
アプリケーションから認証するための情報を作成します。
左側のメニューから「APIとサービス」→ 「OAuth 同意画面」を選択します。
OAuth同意画面の User Type は「外部」を選択後、「作成」ボタンをクリックします。
OAuth 同意画面のアプリ情報、デベロッパーの連絡先情報を入力します。
認可コード、scope、stateをMagic Leap 1のアプリケーションに返却するためのページを私のgithubページ配下に作成しています。その為、承認済みドメインには「tokufxug.github.io」を追加後、「保存して次へ」ボタンをクリックします。
スコープでは、何も設定しないまま「保存して次へ」ボタンをクリックします。
テストユーザーでは、何も設定しないまま「保存して次へ」ボタンをクリックします。
左側のメニューから「APIとサービス」→ 「認証情報」を選択します。
認証情報画面内の左側辺りにある「+認証情報を作成」をクリック。メニューが表示されます。「OAuth クライアントID」を選択してください。
OAuth クライアントIDを作成します。以下の通りに入力してください。
今回作成する OAuth Windowのアプリケーションに認可コード、scope、stateを返却するためのWebページを事前に用意してました。URIについては、このWebページのURLを設定しています。
OAuthクライアントが作成されると「クライアントID」と「クライアントシークレット」が作成されます。これらの情報は後で使用するため、どこかに保存しておいてください。
構築 / 実装
ここではUnity Editorを使用し、Magic Leap 1上で動作するOAuth Windowのサンプルアプリケーションを構築します。
Privileges(特権)
以下の特権が必要です。
API Level
Level 8 以上の設定が必要です。
1. OAuth Window サンプルプログラム Unity Package の インポート
OAuth Windowを使い、Google アカウントのユーザー情報を取得するサンプルプログラムが含まれたUnity Packageを以下からダウンロードします。
https://drive.google.com/file/d/1AQmWHHwnQbQ64LVnc9kYBjoBpf11t2rX/view?usp=sharing
ダウンロードしたUnity Packageをインポートします。
インポート後、Assets/Magic Leap Google OAuth/Scene/にあるGoogleOAuthDemo.unityを開きます。
ヒエラルキーの[Example]/GoogleOAuthExampleを選択します。
2. クライアントID と クライアントシークレットの設定
インスペクタの Client ID と Client Secret は、Google Developer Console のOAuth クライアントID で作成時に発行された「クライアントID」と「クライアントシークレット」を設定します。
3. アプリケーションのビルド
Magic Leap 1にビルドする設定~ビルドまでを行います。
Player Settings の Magic Leap Manifest Settings にある Internet、SecureBrowserWindow、 LocalAreaNetworkにチェックを入れます。次にMagic Leap 1をPCに接続して、Build And Run を実行します。
4. 動作確認
実装ポイント
ここではOAuth Window ~ ユーザー情報取得までの処理について、説明します。
1. Google 認証画面 URL の 作成
void Awake()
{
state = GetRandomStringForUrl(32);
oauthURL = string.Format(
"https://accounts.google.com/o/oauth2/v2/auth?client_id={0}&redirect_uri={1}&scope={2}&code_challenge={3}&state={4}&&code_challenge_method=S256&response_type=code&access_type=offline&prompt=consent&session=false"
, clientId
, redirectUri
, "https://www.googleapis.com/auth/userinfo.profile"
, GetCodeChallenge()
, state
);
}
OAuth Window上で開くGoogle 認証画面 の URLを作成します。
2. OAuth Window を開く
void Start()
{
MLInput.OnControllerButtonUp += OnButtonUp;
oAuthEvent += OAuthCallback;
}
void OnDestroy()
{
MLInput.OnControllerButtonUp -= OnButtonUp;
oAuthEvent -= OAuthCallback;
}
void OnButtonUp(byte controllerID, MLInput.Controller.Button button)
{
if (button == MLInput.Controller.Button.Bumper)
{
string refreshToken = GetRefreshToken();
if (String.IsNullOrEmpty(refreshToken))
{
MLResult result = MLDispatch.OAuthRegisterSchema(customURLSchemeRedirectUri, ref oAuthEvent);
MLDispatch.OAuthRegisterSchema(cancelUri, ref oAuthEvent);
MLDispatch.OAuthOpenWindow(oauthURL, cancelUri);
}
else
{
StartCoroutine(RetrieveAccessToken(refreshToken));
}
}
else if (button == MLInput.Controller.Button.HomeTap)
{
MLDispatch.OAuthUnRegisterSchema(customURLSchemeRedirectUri);
MLDispatch.OAuthUnRegisterSchema(cancelUri);
}
}
3. OAuthのコールバック
void OAuthCallback(string response, string schema)
{
if (schema == customURLSchemeRedirectUri)
{
var authResponse = response.Replace(customURLSchemeRedirectUri, "");
var authJson = JsonUtility.FromJson<AuthResult>(authResponse);
if (authJson.State == state)
{
StartCoroutine(RetrieveAccessTokenAndRefreshToken(authJson.Code));
}
else
{
var msg = "Error: Inconsistent State during authentication.";
DataText.text = msg;
throw new Exception(msg);
}
}
else if (schema == cancelUri)
{
DataText.text = "Cancel";
}
}
private string customURLSchemeRedirectUri = "redirecturi://";
private string cancelUri = "canceluri://";
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Magic Leap 1 OAuth Sample</title>
</head>
<script type="text/javascript">
function onRedirectOAuth()
{
var params = new URLSearchParams(window.location.search);
var val = '';
if (params.get('state'))
{
val = 'redirecturi://{"code" : "' + params.get('code') + '", "scope" : "' + params.get('scope') + '", "state" : "' + params.get('state') + '"}';
}
else
{
val = 'redirecturi://{"code" : "' + params.get('code') + '", "scope" : "' + params.get('scope') + '"}';
}
console.log(val);
location.href = val;
}
</script>
<body>
<center>
<a href="#" onclick="onRedirectOAuth();">Return to the application</a>
</center>
</body>
</html>
if (schema == customURLSchemeRedirectUri)
{
var authResponse = response.Replace(customURLSchemeRedirectUri, "");
var authJson = JsonUtility.FromJson<AuthResult>(authResponse);
if (authJson.State == state)
{
StartCoroutine(RetrieveAccessTokenAndRefreshToken(authJson.Code));
}
else
{
var msg = "Error: Inconsistent State during authentication.";
DataText.text = msg;
throw new Exception(msg);
}
}
4. アクセストークンとリフレッシュトークンの取得~ユーザー情報の取得
アクセストークンとリフレッシュトークンの取得~ユーザー情報の取得方法についての説明は、Magic Leap 1が提供するOAuth Windowによる処理ではないため割愛します。今回のメインとなるソースコードは以下になります。(OAuth Window サンプルプログラム Unity Packageに含まれているソースコードと同一のものになります。)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using UnityEngine;
using UnityEngine.XR.MagicLeap;
using UnityEngine.UI;
using UnityEngine.Networking;
namespace MagicLeapGoogleOAuth
{
public class GoogleOAuthExample : MonoBehaviour
{
[Serializable]
public class AuthResult
{
[SerializeField] private string code = default;
public string Code => code;
[SerializeField] private string scope = default;
public string Scope => scope;
[SerializeField] private string state = default;
public string State => state;
}
[Serializable]
public class TokenResult
{
[SerializeField] private string access_token = default;
public string AccessToken => access_token;
[SerializeField] private string expires_in = default;
public string ExpiresIn => expires_in;
[SerializeField] private string id_token = default;
public string IdToken => id_token;
[SerializeField] private string refresh_token = default;
public string RefreshToken => refresh_token;
[SerializeField] private string scope = default;
public string Scope => scope;
[SerializeField] private string token_type = default;
public string TokenType => token_type;
}
[Serializable]
public class UserInfoResult
{
[SerializeField] private string id = default;
public string Id => id;
[SerializeField] private string name = default;
public string Name => name;
[SerializeField] private string given_name = default;
public string GivenName => given_name;
[SerializeField] private string family_name = default;
public string FamilyName => family_name;
[SerializeField] private string picture = default;
public string Picture => picture;
[SerializeField] private string locale = default;
public string Locale => locale;
}
// Have UI Text available for the hierarchy.
[SerializeField]
private Text DataText;
[SerializeField]
private RawImage UserInfoPicture;
// Client ID in your Google Cloud Platform credentials
[SerializeField]
private string clientId;
// Client Secret in your Google Cloud Platform credentials
[SerializeField]
private string clientSecret;
[SerializeField]
private string redirectUri = "https://tokufxug.github.io/ML-OAuth-Redirect/mloauth.html";
private string customURLSchemeRedirectUri = "redirecturi://";
private string cancelUri = "canceluri://";
private string oauthURL = "";
private string state = "";
private string codeVerifier = "";
private const string REFRESH_TOKEN_KEY = "refresh_token_key";
// Start listening for OAuth dispatch events
public event MLDispatch.OAuthHandler oAuthEvent;
void Awake()
{
state = GetRandomStringForUrl(32);
oauthURL = string.Format(
"https://accounts.google.com/o/oauth2/v2/auth?client_id={0}&redirect_uri={1}&scope={2}&code_challenge={3}&state={4}&&code_challenge_method=S256&response_type=code&access_type=offline&prompt=consent&session=false"
, clientId
, redirectUri
, "https://www.googleapis.com/auth/userinfo.profile"
, GetCodeChallenge()
, state
);
}
void Start()
{
MLInput.OnControllerButtonUp += OnButtonUp;
oAuthEvent += OAuthCallback;
}
private void OnDestroy()
{
MLInput.OnControllerButtonUp -= OnButtonUp;
oAuthEvent -= OAuthCallback;
}
void OnButtonUp(byte controllerID, MLInput.Controller.Button button)
{
if (button == MLInput.Controller.Button.Bumper)
{
string refreshToken = GetRefreshToken();
if (String.IsNullOrEmpty(refreshToken))
{
MLResult result = MLDispatch.OAuthRegisterSchema(customURLSchemeRedirectUri, ref oAuthEvent);
MLDispatch.OAuthRegisterSchema(cancelUri, ref oAuthEvent);
MLDispatch.OAuthOpenWindow(oauthURL, cancelUri);
}
else
{
StartCoroutine(RetrieveAccessToken(refreshToken));
}
}
else if (button == MLInput.Controller.Button.HomeTap)
{
MLDispatch.OAuthUnRegisterSchema(customURLSchemeRedirectUri);
MLDispatch.OAuthUnRegisterSchema(cancelUri);
}
}
string GetRefreshToken()
{
byte[] data = new byte[0];
string refreshToken = "";
try
{
MLSecureStorage.GetData(REFRESH_TOKEN_KEY, ref data);
refreshToken = System.Text.Encoding.UTF8.GetString(data);
}
catch (Exception e){}
return refreshToken;
}
// Show callbacks
void OAuthCallback(string response, string schema)
{
if (schema == customURLSchemeRedirectUri)
{
var authResponse = response.Replace(customURLSchemeRedirectUri, "");
var authJson = JsonUtility.FromJson<AuthResult>(authResponse);
if (authJson.State == state)
{
StartCoroutine(RetrieveAccessTokenAndRefreshToken(authJson.Code));
}
else
{
var msg = "Error: Inconsistent State during authentication.";
DataText.text = msg;
throw new Exception(msg);
}
}
else if (schema == cancelUri)
{
DataText.text = "Cancel";
}
}
IEnumerator RetrieveAccessTokenAndRefreshToken(string code)
{
var form = new WWWForm();
form.AddField("client_id", clientId);
form.AddField("client_secret", clientSecret);
form.AddField("code", code);
form.AddField("code_verifier", codeVerifier);
form.AddField("grant_type", "authorization_code");
form.AddField("redirect_uri", redirectUri);
UnityWebRequest request = UnityWebRequest.Post("https://oauth2.googleapis.com/token", form);
request.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
DataText.text = request.error + " " + request.downloadHandler.text;
}
else
{
var response = JsonUtility.FromJson<TokenResult>(request.downloadHandler.text);
try
{
MLSecureStorage.StoreData(REFRESH_TOKEN_KEY, Encoding.UTF8.GetBytes(response.RefreshToken));
StartCoroutine(GetUserInfo(response.AccessToken));
}
catch (Exception e)
{
DataText.text += e.ToString() + e.Message;
}
}
}
IEnumerator RetrieveAccessToken(string refreshToken)
{
var form = new WWWForm();
form.AddField("client_id", clientId);
form.AddField("client_secret", clientSecret);
form.AddField("grant_type", "refresh_token");
form.AddField("refresh_token", refreshToken);
UnityWebRequest request = UnityWebRequest.Post("https://oauth2.googleapis.com/token", form);
request.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
yield return request.SendWebRequest();
var data = ResultResponseData(request);
if (data != null)
{
var response = JsonUtility.FromJson<TokenResult>(data.text);
StartCoroutine(GetUserInfo(response.AccessToken));
}
}
IEnumerator GetUserInfo(string accessToken)
{
var form = new WWWForm();
UnityWebRequest request = UnityWebRequest.Get(
"https://www.googleapis.com/oauth2/v1/userinfo?access_token=" + accessToken);
yield return request.SendWebRequest();
var data = ResultResponseData(request);
if (data != null)
{
var response = JsonUtility.FromJson<UserInfoResult>(data.text);
DataText.text = string.Format("ID:{0}\nName:{1}\nLocale:{2}", response.Id, response.Name, response.Locale);
StartCoroutine(GetPicture(response.Picture));
}
}
IEnumerator GetPicture(string picture)
{
var form = new WWWForm();
UnityWebRequest request = UnityWebRequestTexture.GetTexture(picture);
yield return request.SendWebRequest();
var data = ResultResponseData(request);
if (data != null)
{
UserInfoPicture.gameObject.SetActive(true);
UserInfoPicture.texture = ((DownloadHandlerTexture)data).texture;
}
}
private string GetCodeChallenge()
{
codeVerifier = GetRandomStringForUrl(32);
return ConvertToBase64Url(Sha256(codeVerifier));
}
private string GetRandomStringForUrl(uint length)
{
var cryptoServiceProvider = new RNGCryptoServiceProvider();
var bytes = new byte[length];
cryptoServiceProvider.GetBytes(bytes);
return ConvertToBase64Url(bytes);
}
private string ConvertToBase64Url(byte[] bytes)
{
return Convert.ToBase64String(bytes)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "");
}
private static byte[] Sha256(string source)
{
var bytes = Encoding.ASCII.GetBytes(source);
return new SHA256Managed().ComputeHash(bytes);
}
private DownloadHandler ResultResponseData(UnityWebRequest request)
{
DownloadHandler data = null;
switch (request.result)
{
case UnityWebRequest.Result.Success:
data = request.downloadHandler;
break;
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.ProtocolError:
case UnityWebRequest.Result.DataProcessingError:
DataText.text = request.error + " " + request.downloadHandler.text;
break;
case UnityWebRequest.Result.InProgress:
break;
default:
DataText.text = "ArgumentOutOfRangeException";
throw new ArgumentOutOfRangeException();
}
return data;
}
}
}
まとめ
参考
Magic Leap Developer Portal OAuth Windows - Unity https://developer.magicleap.com/ja-jp/learn/guides/sdk-oauth-windows-for-unity
【Unity】UnityエディタでGoogle APIのOAuth2認証をする(ライブラリ使わない版)https://light11.hatenadiary.com/entry/2020/05/05/183325
最後に
弊社では、Magic Leap 1を活用したアプリケーションをMagic Leap Worldに2つリリースしています!
OnePlanet XR
「OnePlanet XR」はAR/MR技術に専門特化したコンサルティングサービスです。豊富な実績を元に、AR/MR技術を活用した新たな事業の立ち上げ支援や、社内業務のデジタル化/DX推進など、貴社の必要とするイノベーションを実現いたします。
MRグラスを活用した3Dモデル設置シミュレーション
ご相談から受け付けております。ご興味ございましたら弊社までお問い合わせください。
お問い合わせ先: https://1planet.co.jp/xrconsulting.html
OnePlanet Tech Magazine
https://note.com/oneplanetinc/m/m25ceb06130d0