From d5545d8f49f2ee6299572497588819e8d6c7cdc5 Mon Sep 17 00:00:00 2001 From: "paul.vickery" Date: Fri, 16 Oct 2020 13:37:56 +0100 Subject: [PATCH 1/2] Set output box to be anchored to the window, so it resizes with it, to make it easier to examine the results --- OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml b/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml index b4a5765..1c8d451 100755 --- a/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml +++ b/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml @@ -22,13 +22,13 @@ limitations under the License. mc:Ignorable="d" Title="MainWindow" Height="350" Width="525" > - - - + + - + From aec812aeb2ae26231bc915a8e979830abd679686 Mon Sep 17 00:00:00 2001 From: "paul.vickery" Date: Wed, 13 Jan 2021 14:26:33 +0000 Subject: [PATCH 2/2] Got it all working with WPF / MVVM --- .../OAuthDesktopApp/GoogleApi/ApiCalls.cs | 46 +++ .../GoogleApi/GoogleApiBase.cs | 19 + .../OAuthDesktopApp/GoogleApi/GoogleOAuth2.cs | 172 ++++++++ .../OAuthDesktopApp/MainWindow.xaml | 11 +- .../OAuthDesktopApp/MainWindow.xaml.cs | 375 ++++++------------ .../OAuthDesktopApp/OAuthDesktopApp.csproj | 10 +- .../Utilities/OAuthException.cs | 24 ++ .../OAuthDesktopApp/Utilities/OAuthHelpers.cs | 64 +++ .../OAuthDesktopApp/packages.config | 2 +- 9 files changed, 461 insertions(+), 262 deletions(-) create mode 100644 OAuthDesktopApp/OAuthDesktopApp/GoogleApi/ApiCalls.cs create mode 100644 OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleApiBase.cs create mode 100644 OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleOAuth2.cs create mode 100644 OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthException.cs create mode 100644 OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthHelpers.cs diff --git a/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/ApiCalls.cs b/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/ApiCalls.cs new file mode 100644 index 0000000..66e6090 --- /dev/null +++ b/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/ApiCalls.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; + +namespace OAuthDesktopApp.GoogleApi +{ + public class ApiCalls : GoogleApiBase + { + private const string UserInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo"; + + public ApiCalls(Action outputCallback) : base(outputCallback) + { + } + + public async Task UserinfoCallAsync(GoogleOAuth2 oAuth2) + { + Output("Making API Call to Userinfo..."); + + // builds the request + string userinfoRequestURI = UserInfoEndpoint; + + // sends the request + HttpWebRequest userinfoRequest = (HttpWebRequest)WebRequest.Create(userinfoRequestURI); + userinfoRequest.Method = "GET"; + userinfoRequest.Headers.Add(string.Format("Authorization: Bearer {0}", oAuth2.AccessToken)); + userinfoRequest.ContentType = "application/x-www-form-urlencoded"; + userinfoRequest.Accept = "Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; + + // gets the response + WebResponse userinfoResponse = await userinfoRequest.GetResponseAsync(); + using (StreamReader userinfoResponseReader = new StreamReader(userinfoResponse.GetResponseStream())) + { + // reads response body + string userinfoResponseText = await userinfoResponseReader.ReadToEndAsync(); + Output(userinfoResponseText); + + return userinfoResponseText; + } + } + + // TODO: add refresh token code + + // TODO: make calling code generic + } +} diff --git a/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleApiBase.cs b/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleApiBase.cs new file mode 100644 index 0000000..79fde2f --- /dev/null +++ b/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleApiBase.cs @@ -0,0 +1,19 @@ +using System; + +namespace OAuthDesktopApp.GoogleApi +{ + public abstract class GoogleApiBase + { + private readonly Action outputCallback; + + protected GoogleApiBase(Action outputCallback) + { + this.outputCallback = outputCallback; + } + + protected void Output(string text) + { + this.outputCallback?.Invoke(text); + } + } +} diff --git a/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleOAuth2.cs b/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleOAuth2.cs new file mode 100644 index 0000000..b34b7ce --- /dev/null +++ b/OAuthDesktopApp/OAuthDesktopApp/GoogleApi/GoogleOAuth2.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using OAuthDesktopApp.Utilities; + +namespace OAuthDesktopApp.GoogleApi +{ + public class GoogleOAuth2 : GoogleApiBase + { + // client configuration + private const string ClientID = "581786658708-elflankerquo1a6vsckabbhn25hclla0.apps.googleusercontent.com"; + private const string ClientSecret = "3f6NggMbPtrmIBpgx-MK2xXK"; + private const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + private const string TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"; + private const string ResponseString = "Done" + + "Please return to the app.

If this window does not close automatically, please close manually.

"; + private const string CodeChallengeMethod = "S256"; + + public string AccessToken { get; private set; } + + public string RefreshToken { get; private set; } + + public GoogleOAuth2(Action outputCallback) : base(outputCallback) + { + } + + public async Task PerformAuthenticationAsync(Action activateCallback) + { + // Clear any previous tokens + this.AccessToken = null; + this.RefreshToken = null; + + // Generates state and PKCE values. + string state = OAuthHelpers.RandomDataBase64url(32); + string codeVerifier = OAuthHelpers.RandomDataBase64url(32); + string codeChallenge = OAuthHelpers.Base64urlencodeNoPadding(OAuthHelpers.Sha256(codeVerifier)); + + // Creates a redirect URI using an available port on the loopback address. + string redirectURI = string.Format("http://{0}:{1}/", IPAddress.Loopback, OAuthHelpers.GetRandomUnusedPort()); + Output("redirect URI: " + redirectURI); + + // Creates an HttpListener to listen for requests on that redirect URI. + var http = new HttpListener(); + http.Prefixes.Add(redirectURI); + Output("Listening.."); + http.Start(); + + // Creates the OAuth 2.0 authorization request. + string authorizationRequest = string.Format("{0}?response_type=code&scope=openid%20profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}", + AuthorizationEndpoint, + System.Uri.EscapeDataString(redirectURI), + ClientID, + state, + codeChallenge, + CodeChallengeMethod); + + // Opens request in the browser. + System.Diagnostics.Process.Start(authorizationRequest); + + // Waits for the OAuth authorization response. + var context = await http.GetContextAsync(); + + // Brings this app back to the foreground. + activateCallback?.Invoke(); + + // Sends an HTTP response to the browser. + var response = context.Response; + + var buffer = Encoding.UTF8.GetBytes(ResponseString); + response.ContentLength64 = buffer.Length; + var responseOutput = response.OutputStream; + Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) => + { + responseOutput.Close(); + http.Stop(); + Console.WriteLine("HTTP server stopped."); + }); + + // Checks for errors. + if (context.Request.QueryString.Get("error") != null) + { + throw new OAuthException(string.Format("OAuth authorization error: {0}.", context.Request.QueryString.Get("error"))); + } + + if (context.Request.QueryString.Get("code") == null + || context.Request.QueryString.Get("state") == null) + { + throw new OAuthException("Malformed authorization response. " + context.Request.QueryString); + } + + // extracts the code + var code = context.Request.QueryString.Get("code"); + var incoming_state = context.Request.QueryString.Get("state"); + + // Compares the receieved state to the expected value, to ensure that + // this app made the request which resulted in authorization. + if (incoming_state != state) + { + throw new OAuthException(string.Format("Received request with invalid state ({0})", incoming_state)); + } + + Output("Authorization code: " + code); + + // Starts the code exchange at the Token Endpoint. + await PerformCodeExchange(code, codeVerifier, redirectURI); + } + + private async Task PerformCodeExchange(string code, string code_verifier, string redirectURI) + { + Output("Exchanging code for tokens..."); + + // builds the request + string tokenRequestURI = TokenEndpoint; + string tokenRequestBody = string.Format("code={0}&redirect_uri={1}&client_id={2}&code_verifier={3}&client_secret={4}&scope=&grant_type=authorization_code", + code, + System.Uri.EscapeDataString(redirectURI), + ClientID, + code_verifier, + ClientSecret + ); + + // sends the request + HttpWebRequest tokenRequest = (HttpWebRequest)WebRequest.Create(tokenRequestURI); + tokenRequest.Method = "POST"; + tokenRequest.ContentType = "application/x-www-form-urlencoded"; + tokenRequest.Accept = "Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; + byte[] _byteVersion = Encoding.ASCII.GetBytes(tokenRequestBody); + tokenRequest.ContentLength = _byteVersion.Length; + Stream stream = tokenRequest.GetRequestStream(); + await stream.WriteAsync(_byteVersion, 0, _byteVersion.Length); + stream.Close(); + + try + { + // gets the response + WebResponse tokenResponse = await tokenRequest.GetResponseAsync(); + using (StreamReader reader = new StreamReader(tokenResponse.GetResponseStream())) + { + // reads response body + string responseText = await reader.ReadToEndAsync(); + Output(responseText); + + // converts to dictionary + Dictionary tokenEndpointDecoded = JsonConvert.DeserializeObject>(responseText); + + this.AccessToken = tokenEndpointDecoded["access_token"]; + this.RefreshToken = tokenEndpointDecoded["refresh_token"]; + } + } + catch (WebException ex) + { + if (ex.Status == WebExceptionStatus.ProtocolError) + { + if (ex.Response is HttpWebResponse response) + { + Output("HTTP: " + response.StatusCode); + using (StreamReader reader = new StreamReader(response.GetResponseStream())) + { + // reads response body + string responseText = await reader.ReadToEndAsync(); + Output(responseText); + } + } + } + } + } + } +} diff --git a/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml b/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml index 1c8d451..26bd0be 100755 --- a/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml +++ b/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml @@ -23,12 +23,19 @@ limitations under the License. Title="MainWindow" Height="350" Width="525" > - - + diff --git a/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs b/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs index 68c9138..2dec93b 100755 --- a/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs +++ b/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs @@ -12,265 +12,128 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Newtonsoft.Json; using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; +using System.ComponentModel; using System.Windows; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Newtonsoft.Json.Linq; +using OAuthDesktopApp.GoogleApi; namespace OAuthApp { - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : Window - { - // client configuration - const string clientID = "581786658708-elflankerquo1a6vsckabbhn25hclla0.apps.googleusercontent.com"; - const string clientSecret = "3f6NggMbPtrmIBpgx-MK2xXK"; - const string authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"; - const string tokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"; - const string userInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo"; - - public MainWindow() - { - InitializeComponent(); - } - - // ref http://stackoverflow.com/a/3978040 - public static int GetRandomUnusedPort() - { - var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - - private async void button_Click(object sender, RoutedEventArgs e) - { - // Generates state and PKCE values. - string state = randomDataBase64url(32); - string code_verifier = randomDataBase64url(32); - string code_challenge = base64urlencodeNoPadding(sha256(code_verifier)); - const string code_challenge_method = "S256"; - - // Creates a redirect URI using an available port on the loopback address. - string redirectURI = string.Format("http://{0}:{1}/", IPAddress.Loopback, GetRandomUnusedPort()); - output("redirect URI: " + redirectURI); - - // Creates an HttpListener to listen for requests on that redirect URI. - var http = new HttpListener(); - http.Prefixes.Add(redirectURI); - output("Listening.."); - http.Start(); - - // Creates the OAuth 2.0 authorization request. - string authorizationRequest = string.Format("{0}?response_type=code&scope=openid%20profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}", - authorizationEndpoint, - System.Uri.EscapeDataString(redirectURI), - clientID, - state, - code_challenge, - code_challenge_method); - - // Opens request in the browser. - System.Diagnostics.Process.Start(authorizationRequest); - - // Waits for the OAuth authorization response. - var context = await http.GetContextAsync(); - - // Brings this app back to the foreground. - this.Activate(); - - // Sends an HTTP response to the browser. - var response = context.Response; - string responseString = string.Format("Please return to the app."); - var buffer = System.Text.Encoding.UTF8.GetBytes(responseString); - response.ContentLength64 = buffer.Length; - var responseOutput = response.OutputStream; - Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) => - { - responseOutput.Close(); - http.Stop(); - Console.WriteLine("HTTP server stopped."); - }); - - // Checks for errors. - if (context.Request.QueryString.Get("error") != null) - { - output(String.Format("OAuth authorization error: {0}.", context.Request.QueryString.Get("error"))); - return; - } - if (context.Request.QueryString.Get("code") == null - || context.Request.QueryString.Get("state") == null) - { - output("Malformed authorization response. " + context.Request.QueryString); - return; - } - - // extracts the code - var code = context.Request.QueryString.Get("code"); - var incoming_state = context.Request.QueryString.Get("state"); - - // Compares the receieved state to the expected value, to ensure that - // this app made the request which resulted in authorization. - if (incoming_state != state) - { - output(String.Format("Received request with invalid state ({0})", incoming_state)); - return; - } - output("Authorization code: " + code); - - // Starts the code exchange at the Token Endpoint. - performCodeExchange(code, code_verifier, redirectURI); - } - - async void performCodeExchange(string code, string code_verifier, string redirectURI) - { - output("Exchanging code for tokens..."); - - // builds the request - string tokenRequestURI = "https://www.googleapis.com/oauth2/v4/token"; - string tokenRequestBody = string.Format("code={0}&redirect_uri={1}&client_id={2}&code_verifier={3}&client_secret={4}&scope=&grant_type=authorization_code", - code, - System.Uri.EscapeDataString(redirectURI), - clientID, - code_verifier, - clientSecret - ); - - // sends the request - HttpWebRequest tokenRequest = (HttpWebRequest)WebRequest.Create(tokenRequestURI); - tokenRequest.Method = "POST"; - tokenRequest.ContentType = "application/x-www-form-urlencoded"; - tokenRequest.Accept = "Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; - byte[] _byteVersion = Encoding.ASCII.GetBytes(tokenRequestBody); - tokenRequest.ContentLength = _byteVersion.Length; - Stream stream = tokenRequest.GetRequestStream(); - await stream.WriteAsync(_byteVersion, 0, _byteVersion.Length); - stream.Close(); - - try - { - // gets the response - WebResponse tokenResponse = await tokenRequest.GetResponseAsync(); - using (StreamReader reader = new StreamReader(tokenResponse.GetResponseStream())) - { - // reads response body - string responseText = await reader.ReadToEndAsync(); - output(responseText); - - // converts to dictionary - Dictionary tokenEndpointDecoded = JsonConvert.DeserializeObject>(responseText); - - string access_token = tokenEndpointDecoded["access_token"]; - userinfoCall(access_token); - } - } - catch (WebException ex) - { - if (ex.Status == WebExceptionStatus.ProtocolError) - { - var response = ex.Response as HttpWebResponse; - if (response != null) - { - output("HTTP: " + response.StatusCode); - using (StreamReader reader = new StreamReader(response.GetResponseStream())) - { - // reads response body - string responseText = await reader.ReadToEndAsync(); - output(responseText); - } - } - - } - } - } - - - async void userinfoCall(string access_token) - { - output("Making API Call to Userinfo..."); - - // builds the request - string userinfoRequestURI = "https://www.googleapis.com/oauth2/v3/userinfo"; - - // sends the request - HttpWebRequest userinfoRequest = (HttpWebRequest)WebRequest.Create(userinfoRequestURI); - userinfoRequest.Method = "GET"; - userinfoRequest.Headers.Add(string.Format("Authorization: Bearer {0}", access_token)); - userinfoRequest.ContentType = "application/x-www-form-urlencoded"; - userinfoRequest.Accept = "Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; - - // gets the response - WebResponse userinfoResponse = await userinfoRequest.GetResponseAsync(); - using (StreamReader userinfoResponseReader = new StreamReader(userinfoResponse.GetResponseStream())) - { - // reads response body - string userinfoResponseText = await userinfoResponseReader.ReadToEndAsync(); - output(userinfoResponseText); - } - } - - /// - /// Appends the given string to the on-screen log, and the debug console. - /// - /// string to be appended - public void output(string output) - { - textBoxOutput.Text = textBoxOutput.Text + output + Environment.NewLine; - Console.WriteLine(output); - } - - /// - /// Returns URI-safe data with a given input length. - /// - /// Input length (nb. output will be longer) - /// - public static string randomDataBase64url(uint length) - { - RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); - byte[] bytes = new byte[length]; - rng.GetBytes(bytes); - return base64urlencodeNoPadding(bytes); - } - - /// - /// Returns the SHA256 hash of the input string. - /// - /// - /// - public static byte[] sha256(string inputStirng) - { - byte[] bytes = Encoding.ASCII.GetBytes(inputStirng); - SHA256Managed sha256 = new SHA256Managed(); - return sha256.ComputeHash(bytes); - } - - /// - /// Base64url no-padding encodes the given input buffer. - /// - /// - /// - public static string base64urlencodeNoPadding(byte[] buffer) - { - string base64 = Convert.ToBase64String(buffer); - - // Converts base64 to base64url. - base64 = base64.Replace("+", "-"); - base64 = base64.Replace("/", "_"); - // Strips padding. - base64 = base64.Replace("=", ""); - - return base64; - } - - } + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public class ViewModel : INotifyPropertyChanged + { + private string name; + private Uri imageUrl; + private ImageSource imageSource; + private string outputText; + + public string Name { get { return this.name; } set { this.name = value; OnNotifyPropertyChanged(nameof(Name)); } } + + public Uri ImageUrl { get { return this.imageUrl; } set { this.imageUrl = value; OnNotifyPropertyChanged(nameof(ImageUrl)); } } + + public ImageSource ImageSource { get { return this.imageSource; } set { this.imageSource = value; OnNotifyPropertyChanged(nameof(ImageSource)); } } + + public string OutputText { get { return this.outputText; } set { this.outputText = value; OnNotifyPropertyChanged(nameof(OutputText)); } } + + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnNotifyPropertyChanged(string propertyName) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + private ViewModel viewModel; + + public MainWindow() + { + InitializeComponent(); + + this.viewModel = new ViewModel(); + + //this.label.Content = string.Empty; + //this.label.DataContext = this; + this.DataContext = this.viewModel; + + this.viewModel.Name = "user name goes here"; + this.textBoxOutput.DataContext = this.viewModel; + this.label.DataContext = this.viewModel; + + //this.UserName = "user name goes here"; + } + + private async void Button_Click(object sender, RoutedEventArgs e) + { + try + { + this.viewModel.Name = "getting..."; + var googleAuth = new GoogleOAuth2(this.Output); + await googleAuth.PerformAuthenticationAsync(() => this.Activate()); + + Output("*** Done auth. Making an API call now. ***"); + Output(string.Empty); + + var googleApis = new ApiCalls(this.Output); + var responseJson = await googleApis.UserinfoCallAsync(googleAuth); + + this.UpdateContactInfo(responseJson); + } + catch (Exception ex) + { + Output(ex.Message); + } + } + + private void UpdateContactInfo(string responseJson) + { + /* + { + "sub": "111654053324868073923", + "name": "Paul Vickery", + "given_name": "Paul", + "family_name": "Vickery", + "picture": "https://lh3.googleusercontent.com/a-/AOh14Gg59HW4841ssWsiDbLPHGQIkhv6t9vt-xmBhb2cNg\u003ds96-c", + "locale": "en-GB" + } */ + + JToken json = JToken.Parse(responseJson); + + var name = json.Value("name"); + //var givenName = json.Value("given_name"); + //var familyName = json.Value("family_name"); + var pictureUrl = json.Value("picture"); + + //this.label.Content = name; + this.viewModel.Name = name; + + // https://timheuer.com/blog/making-circular-images-in-xaml-easily/ + // https://stackoverflow.com/questions/23138878/create-circular-image-xaml + var userImage = new BitmapImage(); + userImage.BeginInit(); + userImage.DecodePixelHeight = (int)this.contactImageFrame.Height; + userImage.DecodePixelWidth = (int)this.contactImageFrame.Width; + userImage.UriSource = new Uri(pictureUrl); + userImage.EndInit(); + //this.contactImage.ImageSource = userImage; + this.viewModel.ImageSource = userImage; + } + + /// + /// Appends the given string to the on-screen log, and the debug console. + /// + /// string to be appended + private void Output(string output) + { + //textBoxOutput.Text = textBoxOutput.Text + output + Environment.NewLine; + this.viewModel.OutputText += output + Environment.NewLine; + Console.WriteLine(output); + } + } } diff --git a/OAuthDesktopApp/OAuthDesktopApp/OAuthDesktopApp.csproj b/OAuthDesktopApp/OAuthDesktopApp/OAuthDesktopApp.csproj index 360c655..3fb9769 100755 --- a/OAuthDesktopApp/OAuthDesktopApp/OAuthDesktopApp.csproj +++ b/OAuthDesktopApp/OAuthDesktopApp/OAuthDesktopApp.csproj @@ -35,9 +35,8 @@ 4 - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True + + ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll @@ -59,6 +58,8 @@ MSBuild:Compile Designer + + MSBuild:Compile Designer @@ -67,12 +68,15 @@ App.xaml Code + + MainWindow.xaml Code + Code diff --git a/OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthException.cs b/OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthException.cs new file mode 100644 index 0000000..020f090 --- /dev/null +++ b/OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace OAuthDesktopApp.Utilities +{ + public class OAuthException : Exception + { + public OAuthException() + { + } + + public OAuthException(string message) : base(message) + { + } + + public OAuthException(string message, Exception innerException) : base(message, innerException) + { + } + + protected OAuthException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthHelpers.cs b/OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthHelpers.cs new file mode 100644 index 0000000..f3c5f32 --- /dev/null +++ b/OAuthDesktopApp/OAuthDesktopApp/Utilities/OAuthHelpers.cs @@ -0,0 +1,64 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; + +namespace OAuthDesktopApp.Utilities +{ + internal class OAuthHelpers + { + // ref http://stackoverflow.com/a/3978040 + public static int GetRandomUnusedPort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + /// + /// Returns URI-safe data with a given input length. + /// + /// Input length (nb. output will be longer) + /// + public static string RandomDataBase64url(uint length) + { + RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); + byte[] bytes = new byte[length]; + rng.GetBytes(bytes); + return Base64urlencodeNoPadding(bytes); + } + + /// + /// Returns the SHA256 hash of the input string. + /// + /// + /// + public static byte[] Sha256(string inputString) + { + byte[] bytes = Encoding.ASCII.GetBytes(inputString); + SHA256Managed sha256 = new SHA256Managed(); + return sha256.ComputeHash(bytes); + } + + /// + /// Base64url no-padding encodes the given input buffer. + /// + /// + /// + public static string Base64urlencodeNoPadding(byte[] buffer) + { + string base64 = Convert.ToBase64String(buffer); + + // Converts base64 to base64url. + base64 = base64.Replace("+", "-"); + base64 = base64.Replace("/", "_"); + // Strips padding. + base64 = base64.Replace("=", ""); + + return base64; + } + } +} diff --git a/OAuthDesktopApp/OAuthDesktopApp/packages.config b/OAuthDesktopApp/OAuthDesktopApp/packages.config index adc92ba..fa4b77d 100755 --- a/OAuthDesktopApp/OAuthDesktopApp/packages.config +++ b/OAuthDesktopApp/OAuthDesktopApp/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file