using System; using System.Threading.Tasks; using System.Linq; using System.Collections.Generic; using Xamarin.Utilities; using System.Net; using System.Text; using Xamarin.Auth; using ZicMoove.Droid.OAuth.Xamarin.Utilities; namespace ZicMoove.Droid.OAuth { public class YaOAuth2Authenticator : WebRedirectAuthenticator { string clientId; string clientSecret; string scope; Uri authorizeUrl; Uri accessTokenUrl; Uri redirectUrl; GetUsernameAsyncFunc getUsernameAsync; string requestState; bool reportedForgery = false; /// /// Gets the client identifier. /// /// The client identifier. public string ClientId { get { return this.clientId; } } /// /// Gets the client secret. /// /// The client secret. public string ClientSecret { get { return this.clientSecret; } } /// /// Gets the authorization scope. /// /// The authorization scope. public string Scope { get { return this.scope; } } /// /// Gets the authorize URL. /// /// The authorize URL. public Uri AuthorizeUrl { get { return this.authorizeUrl; } } /// /// Gets the access token URL. /// /// The URL used to request access tokens after an authorization code was received. public Uri AccessTokenUrl { get { return this.accessTokenUrl; } } public new Uri RedirectUrl { get { return this.redirectUrl; } } /// /// Initializes a new /// that authenticates using implicit granting (token). /// /// /// Client identifier. /// /// /// Authorization scope. /// /// /// Authorize URL. /// /// /// Redirect URL. /// /// /// Method used to fetch the username of an account /// after it has been successfully authenticated. /// public YaOAuth2Authenticator(string clientId, string scope, Uri authorizeUrl, Uri redirectUrl, GetUsernameAsyncFunc getUsernameAsync = null) : this (redirectUrl) { if (string.IsNullOrEmpty(clientId)) { throw new ArgumentException("clientId must be provided", "clientId"); } this.clientId = clientId; this.scope = scope ?? ""; if (authorizeUrl == null) { throw new ArgumentNullException("authorizeUrl"); } this.authorizeUrl = authorizeUrl; this.getUsernameAsync = getUsernameAsync; this.accessTokenUrl = null; } /// /// Initializes a new instance /// that authenticates using authorization codes (code). /// /// /// Client identifier. /// /// /// Client secret. /// /// /// Authorization scope. /// /// /// Authorize URL. /// /// /// Redirect URL. /// /// /// URL used to request access tokens after an authorization code was received. /// /// /// Method used to fetch the username of an account /// after it has been successfully authenticated. /// public YaOAuth2Authenticator(string clientId, string clientSecret, string scope, Uri authorizeUrl, Uri redirectUrl, Uri accessTokenUrl, GetUsernameAsyncFunc getUsernameAsync = null) : this (redirectUrl, clientSecret, accessTokenUrl) { if (string.IsNullOrEmpty(clientId)) { throw new ArgumentException("clientId must be provided", "clientId"); } this.clientId = clientId; if (string.IsNullOrEmpty(clientSecret)) { throw new ArgumentException("clientSecret must be provided", "clientSecret"); } this.clientSecret = clientSecret; this.scope = scope ?? ""; if (authorizeUrl == null) { throw new ArgumentNullException("authorizeUrl"); } this.authorizeUrl = authorizeUrl; if (accessTokenUrl == null) { throw new ArgumentNullException("accessTokenUrl"); } this.accessTokenUrl = accessTokenUrl; if (redirectUrl == null) throw new Exception("redirectUrl is null"); this.redirectUrl = redirectUrl; this.getUsernameAsync = getUsernameAsync; } YaOAuth2Authenticator(Uri redirectUrl, string clientSecret = null, Uri accessTokenUrl = null) : base (redirectUrl, redirectUrl) { this.clientSecret = clientSecret; this.accessTokenUrl = accessTokenUrl; // // Generate a unique state string to check for forgeries // var chars = new char[16]; var rand = new Random(); for (var i = 0; i < chars.Length; i++) { chars[i] = (char)rand.Next((int)'a', (int)'z' + 1); } this.requestState = new string(chars); } bool IsImplicit { get { return accessTokenUrl == null; } } /// /// Method that returns the initial URL to be displayed in the web browser. /// /// /// A task that will return the initial URL. /// public override Task GetInitialUrlAsync() { var url = new Uri(string.Format( "{0}?client_id={1}&redirect_uri={2}&response_type={3}&scope={4}&state={5}", authorizeUrl.AbsoluteUri, Uri.EscapeDataString(clientId), Uri.EscapeDataString(RedirectUrl.AbsoluteUri), IsImplicit ? "token" : "code", Uri.EscapeDataString(scope), Uri.EscapeDataString(requestState))); var tcs = new TaskCompletionSource(); tcs.SetResult(url); return tcs.Task; } /// /// Raised when a new page has been loaded. /// /// /// URL of the page. /// /// /// The parsed query of the URL. /// /// /// The parsed fragment of the URL. /// protected override void OnPageEncountered(Uri url, IDictionary query, IDictionary fragment) { if (url.AbsoluteUri.StartsWith(this.redirectUrl.AbsoluteUri)) { // if (!this.redirectUrl.Equals(url)) { // this is not our redirect page, // but perhaps one one the third party identity providers // One don't check for a state here. // /* if (fragment.ContainsKey("continue")) { var cont = fragment["continue"]; // TODO continue browsing this address var tcs = new TaskCompletionSource(); tcs.SetResult(new Uri(cont)); tcs.Task.RunSynchronously(); } return;*/ // } var all = new Dictionary(query); foreach (var kv in fragment) all[kv.Key] = kv.Value; // // Check for forgeries // if (all.ContainsKey("state")) { if (all["state"] != requestState && !reportedForgery) { reportedForgery = true; OnError("Invalid state from server. Possible forgery!"); return; } } } // // Continue processing // base.OnPageEncountered(url, query, fragment); } /// /// Raised when a new page has been loaded. /// /// /// URL of the page. /// /// /// The parsed query string of the URL. /// /// /// The parsed fragment of the URL. /// protected override void OnRedirectPageLoaded(Uri url, IDictionary query, IDictionary fragment) { // // Look for the access_token // if (fragment.ContainsKey("access_token")) { // // We found an access_token // OnRetrievedAccountProperties(fragment); } else if (!IsImplicit) { // // Look for the code // if (query.ContainsKey("code")) { var code = query["code"]; RequestAccessTokenAsync(code).ContinueWith(task => { if (task.IsFaulted) { OnError(task.Exception); } else { OnRetrievedAccountProperties(task.Result); } }, TaskScheduler.FromCurrentSynchronizationContext()); } else { OnError("Expected code in response, but did not receive one."); return; } } else { OnError("Expected access_token in response, but did not receive one."); return; } } /// /// Asynchronously requests an access token with an authorization . /// /// /// A dictionary of data returned from the authorization request. /// /// The authorization code. /// Implements: http://tools.ietf.org/html/rfc6749#section-4.1 Task> RequestAccessTokenAsync(string code) { var queryValues = new Dictionary { { "grant_type", "authorization_code" }, { "code", code }, { "redirect_uri", RedirectUrl.AbsoluteUri }, { "client_id", clientId } }; if (!string.IsNullOrEmpty(clientSecret)) { queryValues["client_secret"] = clientSecret; } return RequestAccessTokenAsync(queryValues); } /// /// Asynchronously makes a request to the access token URL with the given parameters. /// /// The parameters to make the request with. /// The data provided in the response to the access token request. protected Task> RequestAccessTokenAsync(IDictionary queryValues) { var query = queryValues.FormEncode(); var req = WebRequest.Create(accessTokenUrl); (req as HttpWebRequest).Accept = "application/json"; req.Method = "POST"; var body = Encoding.UTF8.GetBytes(query); req.ContentLength = body.Length; req.ContentType = "application/x-www-form-urlencoded"; using (var s = req.GetRequestStream()) { s.Write(body, 0, body.Length); s.Close(); } var auth = req.GetResponseAsync().ContinueWith(task => { if (task.IsCompleted) { var text = task.Result.GetResponseText(); req.Abort(); // Parse the response var data = text.Contains("{") ? WebEx.JsonDecode(text) : WebEx.FormDecode(text); if (data.ContainsKey("error")) { throw new AuthException("Error authenticating: " + data["error"]); } else if (data.ContainsKey("access_token")) { return data; } else { throw new AuthException("Expected access_token in access token response, but did not receive one."); } } else if (task.IsFaulted) { throw new AuthException("Unexpected fault"); } else { throw new AuthException($"Ended: {task.Status}"); } }); return auth; } /// /// Event handler that is fired when an access token has been retreived. /// /// /// The retrieved account properties /// protected virtual void OnRetrievedAccountProperties(IDictionary accountProperties) { // // Now we just need a username for the account // if (getUsernameAsync != null) { getUsernameAsync(accountProperties).ContinueWith(task => { if (task.IsFaulted) { OnError(task.Exception); } else { OnSucceeded(task.Result, accountProperties); } }, TaskScheduler.FromCurrentSynchronizationContext()); } else { OnSucceeded("", accountProperties); } } } // // Copyright 2012, Xamarin Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // namespace Xamarin.Utilities { using System; using System.Net; using System.Threading.Tasks; using System.Collections.Generic; using System.Text; using System.IO; using System.Linq; using System.Globalization; using Newtonsoft.Json.Linq; internal static class WebEx { public static string GetCookie(this CookieContainer containers, Uri domain, string name) { var c = containers .GetCookies(domain) .Cast() .FirstOrDefault(x => x.Name == name); return c != null ? c.Value : ""; } public static Encoding GetEncodingFromContentType(string contentType) { // // TODO: Parse the Content-Type // return Encoding.UTF8; } public static string GetResponseText(this WebResponse response) { var httpResponse = response as HttpWebResponse; var encoding = Encoding.UTF8; if (httpResponse != null) { encoding = GetEncodingFromContentType(response.ContentType); } using (var s = response.GetResponseStream()) { using (var r = new StreamReader(s, encoding)) { return r.ReadToEnd(); } } } public static Task GetResponseAsync(this WebRequest request) { return Task .Factory .FromAsync(request.BeginGetResponse, request.EndGetResponse, null); } static char[] AmpersandChars = new char[] { '&' }; static char[] EqualsChars = new char[] { '=' }; public static IDictionary FormDecode(string encodedString) { var inputs = new Dictionary(); if (encodedString.StartsWith("?") || encodedString.StartsWith("#")) { encodedString = encodedString.Substring(1); } var parts = encodedString.Split(AmpersandChars); foreach (var p in parts) { var kv = p.Split(EqualsChars); var k = Uri.UnescapeDataString(kv[0]); var v = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : ""; inputs[k] = v; } return inputs; } public static Dictionary JsonDecode(string encodedString) { var result = new Dictionary(); var jtoken = JToken.Parse(encodedString); foreach (JProperty st in jtoken) { result.Add(st.Name, st.Value.ToString()) ; } return result; } public static string HtmlEncode(string text) { if (string.IsNullOrEmpty(text)) { return ""; } var sb = new StringBuilder(text.Length); int len = text.Length; for (int i = 0; i < len; i++) { switch (text[i]) { case '<': sb.Append("<"); break; case '>': sb.Append(">"); break; case '"': sb.Append("""); break; case '&': sb.Append("&"); break; default: if (text[i] > 159) { sb.Append("&#"); sb.Append(((int)text[i]).ToString(CultureInfo.InvariantCulture)); sb.Append(";"); } else { sb.Append(text[i]); } break; } } return sb.ToString(); } public static string GetValueFromJson(string json, string key) { var p = json.IndexOf("\"" + key + "\""); if (p < 0) return ""; var c = json.IndexOf(":", p); if (c < 0) return ""; var q = json.IndexOf("\"", c); if (q < 0) return ""; var b = q + 1; var e = b; for (; e < json.Length && json[e] != '\"'; e++) { } var r = json.Substring(b, e - b); return r; } } } }