using System; using System.Threading.Tasks; using System.Collections.Generic; using System.Net; using System.Text; using GetUsernameAsyncFunc=System.Func, System.Threading.Tasks.Task>; using System.IO; using Newtonsoft.Json; namespace Yavsc.Authentication { public class OAuthenticator { public OAuthenticator() { } 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; } } /// /// Redirect Url /// public 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 OAuthenticator(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"); } if (authorizeUrl==null) throw new ArgumentNullException("authorizeUrl"); this.clientId = clientId; this.scope = scope ?? ""; 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 OAuthenticator(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; } OAuthenticator(Uri redirectUrl, string clientSecret = null, Uri accessTokenUrl = null) { this.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 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 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; } } } } private void OnError(string v) { throw new NotImplementedException(); } private void OnError(AggregateException ex) { throw new NotImplementedException(); } /// /// 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 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. public async Task> RequestAccessTokenAsync(IDictionary queryValues) { StringBuilder postData = new StringBuilder(); if (!queryValues.ContainsKey("client_id")) { postData.Append("client_id="+Uri.EscapeDataString($"{this.clientId}")+"&"); } if (!queryValues.ContainsKey("client_secret")) { postData.Append("client_secret="+Uri.EscapeDataString($"{this.clientSecret}")+"&"); } if (!queryValues.ContainsKey("scope")) { postData.Append("scope="+Uri.EscapeDataString($"{this.scope}")+"&"); } foreach (string key in queryValues.Keys) { postData.Append($"{key}="+Uri.EscapeDataString($"{queryValues[key]}")+"&"); } var req = WebRequest.Create(accessTokenUrl); (req as HttpWebRequest).Accept = "application/json"; req.Method = "POST"; var body = Encoding.UTF8.GetBytes(postData.ToString()); req.ContentLength = body.Length; req.ContentType = "application/x-www-form-urlencoded"; var s = req.GetRequestStream(); s.Write(body, 0, body.Length); var auth = await req.GetResponseAsync(); var repstream = auth.GetResponseStream(); var respReader = new StreamReader(repstream); var text = await respReader.ReadToEndAsync(); req.Abort(); // Parse the response var data = text.Contains("{") ? JsonDecode(text) : FormDecode(text); if (data.ContainsKey("error")) { OnError("Error authenticating: " + data["error"]); } else if (data.ContainsKey("access_token")) { return data; } else { OnError("Expected access_token in access token response, but did not receive one."); } return data; } private IDictionary FormDecode(string text) { throw new NotImplementedException(); } private IDictionary JsonDecode(string text) { return JsonConvert.DeserializeObject>(text); } /// /// 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); } } private void OnSucceeded(string v, IDictionary accountProperties) { throw new NotImplementedException(); } } }