/// <summary> /// Called when a server requests user authorization /// </summary> /// <param name="sender">Server of type <see cref="Server"/> requiring authorization</param> /// <param name="e">Authorization request event arguments</param> public void OnRequestAuthorization(object sender, RequestAuthorizationEventArgs e) { if (!(sender is Server authenticatingServer)) { return; } e.TokenOrigin = RequestAuthorizationEventArgs.TokenOriginType.None; e.AccessToken = null; lock (Properties.Settings.Default.AccessTokenCache) { if (e.SourcePolicy != RequestAuthorizationEventArgs.SourcePolicyType.ForceAuthorization) { var key = authenticatingServer.Base.AbsoluteUri; if (Properties.Settings.Default.AccessTokenCache.TryGetValue(key, out var accessToken)) { if (!e.ForceRefresh && DateTimeOffset.Now < accessToken.Expires) { e.TokenOrigin = RequestAuthorizationEventArgs.TokenOriginType.Saved; e.AccessToken = accessToken; return; } // Token refresh was explicitly requested or the token expired. Refresh it. if (accessToken is InvalidToken) { // Invalid token is not refreshable. Properties.Settings.Default.AccessTokenCache.Remove(key); } else { // Get API endpoints. (Not called from the UI thread or already cached by now. Otherwise it would need to be spawned as a background task to avoid deadlock.) var api = authenticatingServer.GetEndpoints(Window.Abort.Token); // Prepare web request. var request = Xml.Response.CreateRequest(api.TokenEndpoint); try { accessToken = accessToken.RefreshToken(request, null, Window.Abort.Token); // Update access token cache. Properties.Settings.Default.AccessTokenCache[key] = accessToken; // If we got here, return the token. e.TokenOrigin = RequestAuthorizationEventArgs.TokenOriginType.Refreshed; e.AccessToken = accessToken; return; } catch (AccessTokenException ex) { if (ex.ErrorCode == AccessTokenException.ErrorCodeType.InvalidGrant) { // The grant has been revoked. Drop the access token. Properties.Settings.Default.AccessTokenCache.Remove(key); } else { throw; } } } } } if (e.SourcePolicy != RequestAuthorizationEventArgs.SourcePolicyType.SavedOnly) { AuthorizationInProgress = new CancellationTokenSource(); Wizard.TryInvoke((Action)(() => { Wizard.TaskCount++; ReturnPage = Wizard.CurrentPage; Wizard.CurrentPage = this; })); try { // Get API endpoints. (Not called from the UI thread. Otherwise it would need to be spawned as a background task to avoid deadlock.) var api = authenticatingServer.GetEndpoints(Window.Abort.Token); // Prepare new authorization grant. AuthorizationGrant authorizationGrant = null; Uri callbackUri = null; var httpListener = new eduOAuth.HttpListener(IPAddress.Loopback, 0); httpListener.HttpCallback += (object _, HttpCallbackEventArgs eHTTPCallback) => { callbackUri = eHTTPCallback.Uri; AuthorizationInProgress.Cancel(); Wizard.TryInvoke((Action)(() => Wizard.CurrentPage = ReturnPage)); }; httpListener.HttpRequest += (object _, HttpRequestEventArgs eHTTPRequest) => { if (eHTTPRequest.Uri.AbsolutePath.ToLowerInvariant() == "/favicon.ico") { var res = System.Windows.Application.GetResourceStream(new Uri("pack://*****:*****@RETURN_TO@", HttpUtility.UrlEncode(authorizationUri.ToString())) .Replace("@ORG_ID@", HttpUtility.UrlEncode(srv.OrganizationId))); } // Trigger authorization. Process.Start(authorizationUri.ToString()); // Wait for a change: either callback is invoked, either user cancels. CancellationTokenSource.CreateLinkedTokenSource(AuthorizationInProgress.Token, Window.Abort.Token).Token.WaitHandle.WaitOne(); } finally { // Delay HTTP server shutdown allowing browser to finish loading content. new Thread(new ThreadStart(() => { Window.Abort.Token.WaitHandle.WaitOne(5 * 1000); httpListener.Stop(); })).Start(); } if (callbackUri == null) { throw new OperationCanceledException(); } // Get access token from authorization grant. var request = Xml.Response.CreateRequest(api.TokenEndpoint); e.AccessToken = authorizationGrant.ProcessResponse( HttpUtility.ParseQueryString(callbackUri.Query), request, null, Window.Abort.Token); Window.Abort.Token.ThrowIfCancellationRequested(); // Save access token to the cache. e.TokenOrigin = RequestAuthorizationEventArgs.TokenOriginType.Authorized; Properties.Settings.Default.AccessTokenCache[authenticatingServer.Base.AbsoluteUri] = e.AccessToken; } finally { Wizard.TryInvoke((Action)(() => Wizard.TaskCount--)); } } } }
public void AuthorizationGrantTest() { var ag = new AuthorizationGrant( new Uri("https://test.eduvpn.org/?param=1"), new Uri("org.eduvpn.app:/api/callback"), "org.eduvpn.app", new HashSet <string>() { "scope1", "scope2" }); var uriBuilder = new UriBuilder(ag.AuthorizationUri); Assert.AreEqual("https", uriBuilder.Scheme); Assert.AreEqual("test.eduvpn.org", uriBuilder.Host); Assert.AreEqual("/", uriBuilder.Path); var query = HttpUtility.ParseQueryString(uriBuilder.Query); Assert.AreEqual("1", query["param"]); Assert.AreEqual("code", query["response_type"]); Assert.AreEqual("org.eduvpn.app", query["client_id"]); Assert.AreEqual("org.eduvpn.app:/api/callback", query["redirect_uri"]); CollectionAssert.AreEqual(new List <string>() { "scope1", "scope2" }, query["scope"].Split(null)); Assert.IsTrue(AuthorizationGrant.Base64UriDecodeNoPadding(query["state"]).Length > 0); Assert.AreEqual("S256", query["code_challenge_method"]); Assert.IsTrue(AuthorizationGrant.Base64UriDecodeNoPadding(query["code_challenge"]).Length > 0); var request = new Mock <HttpWebRequest>(); request.Setup(obj => obj.RequestUri).Returns(new Uri("https://demo.eduvpn.nl/portal/oauth.php/token")); request.SetupSet(obj => obj.Method = "POST"); request.SetupProperty(obj => obj.Credentials); request.SetupProperty(obj => obj.PreAuthenticate, false); request.SetupSet(obj => obj.ContentType = "application/x-www-form-urlencoded"); request.SetupProperty(obj => obj.ContentLength); var request_buffer = new byte[1048576]; request.Setup(obj => obj.GetRequestStream()).Returns(new MemoryStream(request_buffer, true)); var response = new Mock <HttpWebResponse>(); response.Setup(obj => obj.GetResponseStream()).Returns(new MemoryStream(Encoding.UTF8.GetBytes(Global.AccessTokenJSON))); response.SetupGet(obj => obj.StatusCode).Returns(HttpStatusCode.OK); request.Setup(obj => obj.GetResponse()).Returns(response.Object); AccessToken token1 = new BearerToken(Global.AccessTokenObj, DateTimeOffset.Now), token2 = ag.ProcessResponse(new NameValueCollection() { { "state", query["state"] }, { "code", "1234567890" } }, request.Object, new NetworkCredential("", "password").SecurePassword); var request_param = HttpUtility.ParseQueryString(Encoding.ASCII.GetString(request_buffer, 0, (int)request.Object.ContentLength)); Assert.AreEqual("authorization_code", request_param["grant_type"]); Assert.IsNotNull(request_param["code"]); Assert.AreEqual(ag.RedirectEndpoint, request_param["redirect_uri"]); Assert.AreEqual(ag.ClientId, request_param["client_id"]); Assert.IsNotNull(request_param["code_verifier"]); Assert.AreEqual(token1, token2); Assert.IsTrue((token1.Authorized - token2.Authorized).TotalSeconds < 60); Assert.IsTrue((token1.Expires - token2.Expires).TotalSeconds < 60); Assert.IsTrue(token2.Scope != null); Assert.IsTrue(token1.Scope.SetEquals(token2.Scope)); Assert.ThrowsException <eduJSON.MissingParameterException>(() => ag.ProcessResponse(new NameValueCollection() { { "code", "1234567890" } }, request.Object)); Assert.ThrowsException <eduJSON.MissingParameterException>(() => ag.ProcessResponse(new NameValueCollection() { { "state", query["state"] } }, request.Object)); Assert.ThrowsException <InvalidStateException>(() => ag.ProcessResponse(new NameValueCollection() { { "state", AuthorizationGrant.Base64UrlEncodeNoPadding(new byte[] { 0x01, 0x02, 0x03 }) }, { "code", "1234567890" } }, request.Object)); Assert.ThrowsException <AuthorizationGrantException>(() => ag.ProcessResponse(new NameValueCollection() { { "state", query["state"] }, { "error", "error" }, { "code", "1234567890" } }, request.Object)); Assert.ThrowsException <eduJSON.MissingParameterException>(() => ag.ProcessResponse(new NameValueCollection() { { "state", query["state"] } }, request.Object)); }