No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UniWebViewAuthenticationFlow.cs 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. //
  2. // UniWebViewAuthenticationFlow.cs
  3. // Created by Wang Wei (@onevcat) on 2022-06-25.
  4. //
  5. // This file is a part of UniWebView Project (https://uniwebview.com)
  6. // By purchasing the asset, you are allowed to use this code in as many as projects
  7. // you want, only if you publish the final products under the name of the same account
  8. // used for the purchase.
  9. //
  10. // This asset and all corresponding files (such as source code) are provided on an
  11. // “as is” basis, without warranty of any kind, express of implied, including but not
  12. // limited to the warranties of merchantability, fitness for a particular purpose, and
  13. // noninfringement. In no event shall the authors or copyright holders be liable for any
  14. // claim, damages or other liability, whether in action of contract, tort or otherwise,
  15. // arising from, out of or in connection with the software or the use of other dealing in the software.
  16. //
  17. using System;
  18. using System.Collections;
  19. using UnityEngine.Networking;
  20. using System.Collections.Generic;
  21. using System.Collections.Specialized;
  22. using UnityEngine;
  23. using UnityEngine.Events;
  24. /// <summary>
  25. /// Interface for implementing a custom authentication flow. An authentication flow, in UniWebView, usually a "code" based
  26. /// OAuth 2.0 flow, contains a standard set of steps:
  27. ///
  28. /// 1. User is navigated to a web page that requires authentication;
  29. /// 2. A temporary code is generated by service provider and provided to client by a redirect URL with customized scheme.
  30. /// 3. Client requests an access token using the temporary code by performing an "access token" exchange request.
  31. ///
  32. /// To use the common flow, any customize authentication flow must implement this interface and becomes a subclass of
  33. /// UniWebViewAuthenticationCommonFlow.
  34. /// </summary>
  35. /// <typeparam name="TTokenType"></typeparam>
  36. public interface IUniWebViewAuthenticationFlow<TTokenType>
  37. {
  38. /// <summary>
  39. /// Returns the redirect URL that is used to redirect the user after authenticated. This is used as the `redirect_uri`
  40. /// parameter when navigating user to the authentication page.
  41. ///
  42. /// Usually this is a URL with customize scheme that later service provider may call. It takes intermediate code in its
  43. /// query and can be used to open the current app in client. The native side of UniWebView will catch and handle it,
  44. /// then send it to Unity side as the result of `UniWebViewAuthenticationSession`.
  45. /// </summary>
  46. /// <returns>
  47. /// The redirect URL set in the OAuth settings.
  48. /// </returns>
  49. string GetCallbackUrl();
  50. /// <summary>
  51. /// Returns the config of the authentication flow. It usually defines the authentication requests entry points.
  52. /// </summary>
  53. /// <returns>The config object of an authentication flow.</returns>
  54. UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration();
  55. /// <summary>
  56. /// Returns a dictionary contains the parameters that are used to perform the authentication request.
  57. /// The key value pairs in the dictionary are used to construct the query string of the authentication request.
  58. ///
  59. /// This usually contains fields like `client_id`, `redirect_uri`, `response_type`, etc.
  60. /// </summary>
  61. /// <returns>The dictionary indicates parameters that are used to perform the authentication request.</returns>
  62. Dictionary<string, string> GetAuthenticationUriArguments();
  63. /// <summary>
  64. /// Returns a dictionary contains the parameters that are used to perform the access token exchange request.
  65. /// The key value pairs in the dictionary are used to construct the HTTP form body of the access token exchange request.
  66. /// </summary>
  67. /// <param name="authResponse">
  68. /// The response from authentication request. If the authentication succeeds, it is
  69. /// usually a custom scheme URL with a `code` query as its parameter. Base on this, you could construct the body of the
  70. /// access token exchange request.
  71. /// </param>
  72. /// <returns>
  73. /// The dictionary indicates parameters that are used to perform the access token exchange request.
  74. /// </returns>
  75. Dictionary<string, string> GetAccessTokenRequestParameters(string authResponse);
  76. /// <summary>
  77. /// Returns a dictionary contains the parameters that are used to perform the access token refresh request.
  78. /// The key value pairs in the dictionary are used to construct the HTTP form body of the access token refresh request.
  79. /// </summary>
  80. /// <param name="refreshToken">The refresh token should be used to perform the refresh request.</param>
  81. /// <returns>
  82. /// The dictionary indicates parameters that are used to perform the access token refresh request.
  83. /// </returns>
  84. Dictionary<string, string> GetRefreshTokenRequestParameters(string refreshToken);
  85. /// <summary>
  86. /// Returns the strong-typed token for the authentication process.
  87. ///
  88. /// When the token exchange request finishes without problem, the response body will be passed to this method and
  89. /// any conforming class should construct the token object from the response body.
  90. /// </summary>
  91. /// <param name="exchangeResponse">
  92. /// The body response of the access token exchange request. Usually it contains the desired `access_token` and other
  93. /// necessary fields to describe the authenticated result.
  94. /// </param>
  95. /// <returns>
  96. /// A token object with `TToken` type that represents the authenticated result.
  97. /// </returns>
  98. TTokenType GenerateTokenFromExchangeResponse(string exchangeResponse);
  99. /// <summary>
  100. /// Called when the authentication flow succeeds and a valid token is generated.
  101. /// </summary>
  102. UnityEvent<TTokenType> OnAuthenticationFinished { get; }
  103. /// <summary>
  104. /// Called when any error (including user cancellation) happens during the authentication flow.
  105. /// </summary>
  106. UnityEvent<long, string> OnAuthenticationErrored { get; }
  107. /// <summary>
  108. /// Called when the access token refresh request finishes and a valid refreshed token is generated.
  109. /// </summary>
  110. UnityEvent<TTokenType> OnRefreshTokenFinished { get; }
  111. /// <summary>
  112. /// Called when any error happens during the access token refresh flow.
  113. /// </summary>
  114. UnityEvent<long, string> OnRefreshTokenErrored { get; }
  115. }
  116. /// <summary>
  117. /// The manager object of an authentication flow. This defines and runs the common flow of an authentication process
  118. /// with `code` response type.
  119. /// </summary>
  120. /// <typeparam name="TTokenType">The responsive token type expected for this authentication flow.</typeparam>
  121. public class UniWebViewAuthenticationFlow<TTokenType> {
  122. private IUniWebViewAuthenticationFlow<TTokenType> service;
  123. public UniWebViewAuthenticationFlow(
  124. IUniWebViewAuthenticationFlow<TTokenType> service
  125. )
  126. {
  127. this.service = service;
  128. }
  129. /// <summary>
  130. /// Start the authentication flow.
  131. /// </summary>
  132. public void StartAuth()
  133. {
  134. var callbackUri = new Uri(service.GetCallbackUrl());
  135. var authUrl = GetAuthUrl();
  136. var session = UniWebViewAuthenticationSession.Create(authUrl, callbackUri.Scheme);
  137. var flow = service as UniWebViewAuthenticationCommonFlow;
  138. if (flow != null && flow.privateMode) {
  139. session.SetPrivateMode(true);
  140. }
  141. session.OnAuthenticationFinished += (_, resultUrl) => {
  142. UniWebViewLogger.Instance.Verbose("Auth flow received callback url: " + resultUrl);
  143. ExchangeToken(resultUrl);
  144. };
  145. session.OnAuthenticationErrorReceived += (_, errorCode, message) => {
  146. ExchangeTokenErrored(errorCode, message);
  147. };
  148. UniWebViewLogger.Instance.Verbose("Starting auth flow with url: " + authUrl + "; Callback scheme: " + callbackUri.Scheme);
  149. session.Start();
  150. }
  151. private void ExchangeToken(string response) {
  152. try {
  153. var args = service.GetAccessTokenRequestParameters(response);
  154. var request = GetTokenRequest(args);
  155. MonoBehaviour context = (MonoBehaviour)service;
  156. context.StartCoroutine(SendExchangeTokenRequest(request));
  157. } catch (Exception e) {
  158. var message = e.Message;
  159. var code = -1;
  160. if (e is AuthenticationResponseException ex) {
  161. code = ex.Code;
  162. }
  163. UniWebViewLogger.Instance.Critical("Exception on exchange token response: " + e + ". Code: " + code + ". Message: " + message);
  164. ExchangeTokenErrored(code, message);
  165. }
  166. }
  167. /// <summary>
  168. /// Refresh the access token with the given refresh token.
  169. /// </summary>
  170. /// <param name="refreshToken"></param>
  171. public void RefreshToken(string refreshToken) {
  172. try {
  173. var args = service.GetRefreshTokenRequestParameters(refreshToken);
  174. var request = GetTokenRequest(args);
  175. MonoBehaviour context = (MonoBehaviour)service;
  176. context.StartCoroutine(SendRefreshTokenRequest(request));
  177. } catch (Exception e) {
  178. var message = e.Message;
  179. var code = -1;
  180. if (e is AuthenticationResponseException ex) {
  181. code = ex.Code;
  182. }
  183. UniWebViewLogger.Instance.Critical("Exception on refresh token response: " + e + ". Code: " + code + ". Message: " + message);
  184. RefreshTokenErrored(code, message);
  185. }
  186. }
  187. private string GetAuthUrl() {
  188. var builder = new UriBuilder(service.GetAuthenticationConfiguration().authorizationEndpoint);
  189. var query = new NameValueCollection();
  190. foreach (var kv in service.GetAuthenticationUriArguments()) {
  191. query.Add(kv.Key, kv.Value);
  192. }
  193. builder.Query = UniWebViewAuthenticationUtils.CreateQueryString(query);
  194. return builder.ToString();
  195. }
  196. private UnityWebRequest GetTokenRequest(Dictionary<string, string>args) {
  197. var builder = new UriBuilder(service.GetAuthenticationConfiguration().tokenEndpoint);
  198. var form = new WWWForm();
  199. foreach (var kv in args) {
  200. form.AddField(kv.Key, kv.Value);
  201. }
  202. return UnityWebRequest.Post(builder.ToString(), form);
  203. }
  204. private IEnumerator SendExchangeTokenRequest(UnityWebRequest request) {
  205. return SendTokenRequest(request, ExchangeTokenFinished, ExchangeTokenErrored);
  206. }
  207. private IEnumerator SendRefreshTokenRequest(UnityWebRequest request) {
  208. return SendTokenRequest(request, RefreshTokenFinished, RefreshTokenErrored);
  209. }
  210. private IEnumerator SendTokenRequest(UnityWebRequest request, Action<TTokenType> finishAction, Action<long, string>errorAction) {
  211. using (var www = request) {
  212. yield return www.SendWebRequest();
  213. if (www.result != UnityWebRequest.Result.Success) {
  214. string errorMessage = null;
  215. string errorBody = null;
  216. if (www.error != null) {
  217. errorMessage = www.error;
  218. }
  219. if (www.downloadHandler != null && www.downloadHandler.text != null) {
  220. errorBody = www.downloadHandler.text;
  221. }
  222. UniWebViewLogger.Instance.Critical("Failed to get access token. Error: " + errorMessage + ". " + errorBody);
  223. errorAction(www.responseCode, errorBody ?? errorMessage);
  224. } else {
  225. var responseText = www.downloadHandler.text;
  226. UniWebViewLogger.Instance.Info("Token exchange request succeeded. Response: " + responseText);
  227. try {
  228. var token = service.GenerateTokenFromExchangeResponse(www.downloadHandler.text);
  229. finishAction(token);
  230. } catch (Exception e) {
  231. var message = e.Message;
  232. var code = -1;
  233. if (e is AuthenticationResponseException ex) {
  234. code = ex.Code;
  235. }
  236. UniWebViewLogger.Instance.Critical(
  237. "Exception on parsing token response: " + e + ". Code: " + code + ". Message: " +
  238. message + ". Response: " + responseText);
  239. errorAction(code, message);
  240. }
  241. }
  242. }
  243. }
  244. private void ExchangeTokenFinished(TTokenType token) {
  245. if (service.OnAuthenticationFinished != null) {
  246. service.OnAuthenticationFinished.Invoke(token);
  247. }
  248. service = null;
  249. }
  250. private void ExchangeTokenErrored(long code, string message) {
  251. UniWebViewLogger.Instance.Info("Auth flow errored: " + code + ". Detail: " + message);
  252. if (service.OnAuthenticationErrored != null) {
  253. service.OnAuthenticationErrored.Invoke(code, message);
  254. }
  255. service = null;
  256. }
  257. private void RefreshTokenFinished(TTokenType token) {
  258. if (service.OnRefreshTokenFinished != null) {
  259. service.OnRefreshTokenFinished.Invoke(token);
  260. }
  261. service = null;
  262. }
  263. private void RefreshTokenErrored(long code, string message) {
  264. UniWebViewLogger.Instance.Info("Refresh flow errored: " + code + ". Detail: " + message);
  265. if (service.OnRefreshTokenErrored != null) {
  266. service.OnRefreshTokenErrored.Invoke(code, message);
  267. }
  268. service = null;
  269. }
  270. }
  271. /// <summary>
  272. /// The configuration object of an authentication flow. This defines the authentication entry points.
  273. /// </summary>
  274. public class UniWebViewAuthenticationConfiguration {
  275. internal readonly string authorizationEndpoint;
  276. internal readonly string tokenEndpoint;
  277. /// <summary>
  278. /// Creates a new authentication configuration object with the given entry points.
  279. /// </summary>
  280. /// <param name="authorizationEndpoint">The entry point to navigate end user to for authentication.</param>
  281. /// <param name="tokenEndpoint">The entry point which is used to exchange the received code to a valid access token.</param>
  282. public UniWebViewAuthenticationConfiguration(string authorizationEndpoint, string tokenEndpoint) {
  283. this.authorizationEndpoint = authorizationEndpoint;
  284. this.tokenEndpoint = tokenEndpoint;
  285. }
  286. }
  287. /// <summary>
  288. /// The exception thrown when the authentication flow fails when handling the response.
  289. /// </summary>
  290. public class AuthenticationResponseException : Exception {
  291. /// <summary>
  292. /// Exception error code to identify the error type. See the static instance of this class to know detail of error codes.
  293. /// </summary>
  294. public int Code { get; }
  295. /// <summary>
  296. /// Creates an authentication response exception.
  297. /// </summary>
  298. /// <param name="code">The error code.</param>
  299. /// <param name="message">A message that contains error detail.</param>
  300. public AuthenticationResponseException(int code, string message): base(message) {
  301. Code = code;
  302. }
  303. /// <summary>
  304. /// An unexpected authentication callback is received. Error code 7001.
  305. /// </summary>
  306. public static AuthenticationResponseException UnexpectedAuthCallbackUrl
  307. = new AuthenticationResponseException(7001, "The received callback url is not expected.");
  308. /// <summary>
  309. /// The `state` value in the callback url is not the same as the one in the request. Error code 7002.
  310. /// </summary>
  311. public static AuthenticationResponseException InvalidState
  312. = new AuthenticationResponseException(7002, "The `state` is not valid.");
  313. /// <summary>
  314. /// The response is not a valid one. It does not contains a `code` field or cannot be parsed. Error code 7003.
  315. /// </summary>
  316. /// <param name="query">The query will be delivered as a part of error message.</param>
  317. /// <returns>The created response exception that can be thrown out and handled by package user.</returns>
  318. public static AuthenticationResponseException InvalidResponse(string query) {
  319. return new AuthenticationResponseException(7003, "The service auth response is not valid: " + query);
  320. }
  321. }