// // UniWebViewAuthenticationFlow.cs // Created by Wang Wei (@onevcat) on 2022-06-25. // // This file is a part of UniWebView Project (https://uniwebview.com) // By purchasing the asset, you are allowed to use this code in as many as projects // you want, only if you publish the final products under the name of the same account // used for the purchase. // // This asset and all corresponding files (such as source code) are provided on an // “as is” basis, without warranty of any kind, express of implied, including but not // limited to the warranties of merchantability, fitness for a particular purpose, and // noninfringement. In no event shall the authors or copyright holders be liable for any // claim, damages or other liability, whether in action of contract, tort or otherwise, // arising from, out of or in connection with the software or the use of other dealing in the software. // using System; using System.Collections; using UnityEngine.Networking; using System.Collections.Generic; using System.Collections.Specialized; using UnityEngine; using UnityEngine.Events; /// /// Interface for implementing a custom authentication flow. An authentication flow, in UniWebView, usually a "code" based /// OAuth 2.0 flow, contains a standard set of steps: /// /// 1. User is navigated to a web page that requires authentication; /// 2. A temporary code is generated by service provider and provided to client by a redirect URL with customized scheme. /// 3. Client requests an access token using the temporary code by performing an "access token" exchange request. /// /// To use the common flow, any customize authentication flow must implement this interface and becomes a subclass of /// UniWebViewAuthenticationCommonFlow. /// /// public interface IUniWebViewAuthenticationFlow { /// /// Returns the redirect URL that is used to redirect the user after authenticated. This is used as the `redirect_uri` /// parameter when navigating user to the authentication page. /// /// Usually this is a URL with customize scheme that later service provider may call. It takes intermediate code in its /// query and can be used to open the current app in client. The native side of UniWebView will catch and handle it, /// then send it to Unity side as the result of `UniWebViewAuthenticationSession`. /// /// /// The redirect URL set in the OAuth settings. /// string GetCallbackUrl(); /// /// Returns the config of the authentication flow. It usually defines the authentication requests entry points. /// /// The config object of an authentication flow. UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration(); /// /// Returns a dictionary contains the parameters that are used to perform the authentication request. /// The key value pairs in the dictionary are used to construct the query string of the authentication request. /// /// This usually contains fields like `client_id`, `redirect_uri`, `response_type`, etc. /// /// The dictionary indicates parameters that are used to perform the authentication request. Dictionary GetAuthenticationUriArguments(); /// /// Returns a dictionary contains the parameters that are used to perform the access token exchange request. /// The key value pairs in the dictionary are used to construct the HTTP form body of the access token exchange request. /// /// /// The response from authentication request. If the authentication succeeds, it is /// usually a custom scheme URL with a `code` query as its parameter. Base on this, you could construct the body of the /// access token exchange request. /// /// /// The dictionary indicates parameters that are used to perform the access token exchange request. /// Dictionary GetAccessTokenRequestParameters(string authResponse); /// /// Returns a dictionary contains the parameters that are used to perform the access token refresh request. /// The key value pairs in the dictionary are used to construct the HTTP form body of the access token refresh request. /// /// The refresh token should be used to perform the refresh request. /// /// The dictionary indicates parameters that are used to perform the access token refresh request. /// Dictionary GetRefreshTokenRequestParameters(string refreshToken); /// /// Returns the strong-typed token for the authentication process. /// /// When the token exchange request finishes without problem, the response body will be passed to this method and /// any conforming class should construct the token object from the response body. /// /// /// The body response of the access token exchange request. Usually it contains the desired `access_token` and other /// necessary fields to describe the authenticated result. /// /// /// A token object with `TToken` type that represents the authenticated result. /// TTokenType GenerateTokenFromExchangeResponse(string exchangeResponse); /// /// Called when the authentication flow succeeds and a valid token is generated. /// UnityEvent OnAuthenticationFinished { get; } /// /// Called when any error (including user cancellation) happens during the authentication flow. /// UnityEvent OnAuthenticationErrored { get; } /// /// Called when the access token refresh request finishes and a valid refreshed token is generated. /// UnityEvent OnRefreshTokenFinished { get; } /// /// Called when any error happens during the access token refresh flow. /// UnityEvent OnRefreshTokenErrored { get; } } /// /// The manager object of an authentication flow. This defines and runs the common flow of an authentication process /// with `code` response type. /// /// The responsive token type expected for this authentication flow. public class UniWebViewAuthenticationFlow { private IUniWebViewAuthenticationFlow service; public UniWebViewAuthenticationFlow( IUniWebViewAuthenticationFlow service ) { this.service = service; } /// /// Start the authentication flow. /// public void StartAuth() { var callbackUri = new Uri(service.GetCallbackUrl()); var authUrl = GetAuthUrl(); var session = UniWebViewAuthenticationSession.Create(authUrl, callbackUri.Scheme); var flow = service as UniWebViewAuthenticationCommonFlow; if (flow != null && flow.privateMode) { session.SetPrivateMode(true); } session.OnAuthenticationFinished += (_, resultUrl) => { UniWebViewLogger.Instance.Verbose("Auth flow received callback url: " + resultUrl); ExchangeToken(resultUrl); }; session.OnAuthenticationErrorReceived += (_, errorCode, message) => { ExchangeTokenErrored(errorCode, message); }; UniWebViewLogger.Instance.Verbose("Starting auth flow with url: " + authUrl + "; Callback scheme: " + callbackUri.Scheme); session.Start(); } private void ExchangeToken(string response) { try { var args = service.GetAccessTokenRequestParameters(response); var request = GetTokenRequest(args); MonoBehaviour context = (MonoBehaviour)service; context.StartCoroutine(SendExchangeTokenRequest(request)); } catch (Exception e) { var message = e.Message; var code = -1; if (e is AuthenticationResponseException ex) { code = ex.Code; } UniWebViewLogger.Instance.Critical("Exception on exchange token response: " + e + ". Code: " + code + ". Message: " + message); ExchangeTokenErrored(code, message); } } /// /// Refresh the access token with the given refresh token. /// /// public void RefreshToken(string refreshToken) { try { var args = service.GetRefreshTokenRequestParameters(refreshToken); var request = GetTokenRequest(args); MonoBehaviour context = (MonoBehaviour)service; context.StartCoroutine(SendRefreshTokenRequest(request)); } catch (Exception e) { var message = e.Message; var code = -1; if (e is AuthenticationResponseException ex) { code = ex.Code; } UniWebViewLogger.Instance.Critical("Exception on refresh token response: " + e + ". Code: " + code + ". Message: " + message); RefreshTokenErrored(code, message); } } private string GetAuthUrl() { var builder = new UriBuilder(service.GetAuthenticationConfiguration().authorizationEndpoint); var query = new NameValueCollection(); foreach (var kv in service.GetAuthenticationUriArguments()) { query.Add(kv.Key, kv.Value); } builder.Query = UniWebViewAuthenticationUtils.CreateQueryString(query); return builder.ToString(); } private UnityWebRequest GetTokenRequest(Dictionaryargs) { var builder = new UriBuilder(service.GetAuthenticationConfiguration().tokenEndpoint); var form = new WWWForm(); foreach (var kv in args) { form.AddField(kv.Key, kv.Value); } return UnityWebRequest.Post(builder.ToString(), form); } private IEnumerator SendExchangeTokenRequest(UnityWebRequest request) { return SendTokenRequest(request, ExchangeTokenFinished, ExchangeTokenErrored); } private IEnumerator SendRefreshTokenRequest(UnityWebRequest request) { return SendTokenRequest(request, RefreshTokenFinished, RefreshTokenErrored); } private IEnumerator SendTokenRequest(UnityWebRequest request, Action finishAction, ActionerrorAction) { using (var www = request) { yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { string errorMessage = null; string errorBody = null; if (www.error != null) { errorMessage = www.error; } if (www.downloadHandler != null && www.downloadHandler.text != null) { errorBody = www.downloadHandler.text; } UniWebViewLogger.Instance.Critical("Failed to get access token. Error: " + errorMessage + ". " + errorBody); errorAction(www.responseCode, errorBody ?? errorMessage); } else { var responseText = www.downloadHandler.text; UniWebViewLogger.Instance.Info("Token exchange request succeeded. Response: " + responseText); try { var token = service.GenerateTokenFromExchangeResponse(www.downloadHandler.text); finishAction(token); } catch (Exception e) { var message = e.Message; var code = -1; if (e is AuthenticationResponseException ex) { code = ex.Code; } UniWebViewLogger.Instance.Critical( "Exception on parsing token response: " + e + ". Code: " + code + ". Message: " + message + ". Response: " + responseText); errorAction(code, message); } } } } private void ExchangeTokenFinished(TTokenType token) { if (service.OnAuthenticationFinished != null) { service.OnAuthenticationFinished.Invoke(token); } service = null; } private void ExchangeTokenErrored(long code, string message) { UniWebViewLogger.Instance.Info("Auth flow errored: " + code + ". Detail: " + message); if (service.OnAuthenticationErrored != null) { service.OnAuthenticationErrored.Invoke(code, message); } service = null; } private void RefreshTokenFinished(TTokenType token) { if (service.OnRefreshTokenFinished != null) { service.OnRefreshTokenFinished.Invoke(token); } service = null; } private void RefreshTokenErrored(long code, string message) { UniWebViewLogger.Instance.Info("Refresh flow errored: " + code + ". Detail: " + message); if (service.OnRefreshTokenErrored != null) { service.OnRefreshTokenErrored.Invoke(code, message); } service = null; } } /// /// The configuration object of an authentication flow. This defines the authentication entry points. /// public class UniWebViewAuthenticationConfiguration { internal readonly string authorizationEndpoint; internal readonly string tokenEndpoint; /// /// Creates a new authentication configuration object with the given entry points. /// /// The entry point to navigate end user to for authentication. /// The entry point which is used to exchange the received code to a valid access token. public UniWebViewAuthenticationConfiguration(string authorizationEndpoint, string tokenEndpoint) { this.authorizationEndpoint = authorizationEndpoint; this.tokenEndpoint = tokenEndpoint; } } /// /// The exception thrown when the authentication flow fails when handling the response. /// public class AuthenticationResponseException : Exception { /// /// Exception error code to identify the error type. See the static instance of this class to know detail of error codes. /// public int Code { get; } /// /// Creates an authentication response exception. /// /// The error code. /// A message that contains error detail. public AuthenticationResponseException(int code, string message): base(message) { Code = code; } /// /// An unexpected authentication callback is received. Error code 7001. /// public static AuthenticationResponseException UnexpectedAuthCallbackUrl = new AuthenticationResponseException(7001, "The received callback url is not expected."); /// /// The `state` value in the callback url is not the same as the one in the request. Error code 7002. /// public static AuthenticationResponseException InvalidState = new AuthenticationResponseException(7002, "The `state` is not valid."); /// /// The response is not a valid one. It does not contains a `code` field or cannot be parsed. Error code 7003. /// /// The query will be delivered as a part of error message. /// The created response exception that can be thrown out and handled by package user. public static AuthenticationResponseException InvalidResponse(string query) { return new AuthenticationResponseException(7003, "The service auth response is not valid: " + query); } }