diff options
Diffstat (limited to 'src/DotNetOpenAuth.AspNet')
5 files changed, 646 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs new file mode 100644 index 0000000..deb396f --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADClaims.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.Serialization; + + /// <summary> + /// Contains clains of a AzureAD token. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable in medium trust, it has to be public. + /// </remarks> + [DataContract] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + public class AzureADClaims { + #region Public Properties + + /// <summary> + /// Gets or sets the audience. + /// </summary> + /// <value> The audience token is valid for. </value> + [DataMember(Name = "aud")] + public string Aud { get; set; } + + /// <summary> + /// Gets or sets the issuer. + /// </summary> + /// <value> The issuer. </value> + [DataMember(Name = "iss")] + public string Iss { get; set; } + + /// <summary> + /// Gets or sets the early expiry time. + /// </summary> + /// <value> The early expiry time. </value> + [DataMember(Name = "nbf")] + public string Nbf { get; set; } + + /// <summary> + /// Gets or sets the expiry time. + /// </summary> + /// <value> The expiry time. </value> + [DataMember(Name = "exp")] + public string Exp { get; set; } + + /// <summary> + /// Gets or sets the id of the user. + /// </summary> + /// <value> The id of the user. </value> + [DataMember(Name = "oid")] + public string Oid { get; set; } + + /// <summary> + /// Gets or sets the id of the tenant. + /// </summary> + /// <value> The tenant . </value> + [DataMember(Name = "tid")] + public string Tid { get; set; } + + /// <summary> + /// Gets or sets the appid of application. + /// </summary> + /// <value> The id of the application. </value> + [DataMember(Name = "appid")] + public string Appid { get; set; } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs new file mode 100644 index 0000000..6ff93e7 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs @@ -0,0 +1,457 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.CodeAnalysis; + using System.IdentityModel.Tokens; + using System.IO; + using System.Net; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using System.Web; + using System.Web.Script.Serialization; + using System.Xml; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The AzureAD client. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + public sealed class AzureADClient : OAuth2Client { + #region Constants and Fields + + /// <summary> + /// The authorization endpoint. + /// </summary> + private const string AuthorizationEndpoint = "https://login.windows.net/global/oauth2/authorize"; + + /// <summary> + /// The token endpoint. + /// </summary> + private const string TokenEndpoint = "https://login.windows.net/global/oauth2/token"; + + /// <summary> + /// The name of the graph resource. + /// </summary> + private const string GraphResource = "00000002-0000-0000-c000-000000000000/graph.windows.net"; + + /// <summary> + /// The URL to get the token decoding certificate from. + /// </summary> + private const string MetaDataEndpoint = "https://login.windows.net/evosts.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml"; + + /// <summary> + /// The URL for AzureAD graph. + /// </summary> + private const string GraphEndpoint = "https://graph.windows.net/"; + + /// <summary> + /// The id of the STS. + /// </summary> + private const string STSName = "https://sts.windows.net"; + + /// <summary> + /// The app id. + /// </summary> + private readonly string appId; + + /// <summary> + /// The app secret. + /// </summary> + private readonly string appSecret; + + /// <summary> + /// The resource to target. + /// </summary> + private readonly string resource; + + /// <summary> + /// Encoding cert. + /// </summary> + private static X509Certificate2[] encodingcert; + + /// <summary> + /// Hash algo used by the X509Cert. + /// </summary> + private static HashAlgorithm hash; + + /// <summary> + /// The tenantid claim for the authcode. + /// </summary> + private string tenantid; + + /// <summary> + /// The userid claim for the authcode. + /// </summary> + private string userid; + #endregion + + #region Constructors and Destructors + + /// <summary> + /// Initializes a new instance of the <see cref="AzureADClient"/> class. + /// </summary> + /// <param name="appId"> + /// The app id. + /// </param> + /// <param name="appSecret"> + /// The app secret. + /// </param> + public AzureADClient(string appId, string appSecret) + : this(appId, appSecret, GraphResource) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AzureADClient"/> class. + /// </summary> + /// <param name="appId"> + /// The app id. + /// </param> + /// <param name="appSecret"> + /// The app secret. + /// </param> + /// <param name="resource"> + /// The resource of oauth request. + /// </param> + public AzureADClient(string appId, string appSecret, string resource) + : base("azuread") { + Requires.NotNullOrEmpty(appId, "appId"); + Requires.NotNullOrEmpty(appSecret, "appSecret"); + Requires.NotNullOrEmpty(resource, "resource"); + this.appId = appId; + this.appSecret = appSecret; + this.resource = resource; + } + #endregion + + #region Methods + + /// <summary> + /// The get service login url. + /// </summary> + /// <param name="returnUrl"> + /// The return url. + /// </param> + /// <returns>An absolute URI.</returns> + protected override Uri GetServiceLoginUrl(Uri returnUrl) { + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArgs( + new Dictionary<string, string> { + { "client_id", this.appId }, + { "redirect_uri", returnUrl.AbsoluteUri }, + { "response_type", "code" }, + { "resource", this.resource }, + }); + return builder.Uri; + } + + /// <summary> + /// The get user data. + /// </summary> + /// <param name="accessToken"> + /// The access token. + /// </param> + /// <returns>A dictionary of profile data.</returns> + protected override IDictionary<string, string> GetUserData(string accessToken) { + IDictionary<string, string> userData = new Dictionary<string, string>(); + try { + AzureADGraph graphData; + WebRequest request = + WebRequest.Create( + GraphEndpoint + this.tenantid + "/users/" + this.userid + "?api-version=0.9"); + request.Headers = new WebHeaderCollection(); + request.Headers.Add("authorization", accessToken); + using (var response = request.GetResponse()) { + using (var responseStream = response.GetResponseStream()) { + graphData = JsonHelper.Deserialize<AzureADGraph>(responseStream); + } + } + + // this dictionary must contains + userData.AddItemIfNotEmpty("id", graphData.ObjectId); + userData.AddItemIfNotEmpty("username", graphData.UserPrincipalName); + userData.AddItemIfNotEmpty("name", graphData.DisplayName); + + return userData; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return userData; + } + } + + /// <summary> + /// Obtains an access token given an authorization code and callback URL. + /// </summary> + /// <param name="returnUrl"> + /// The return url. + /// </param> + /// <param name="authorizationCode"> + /// The authorization code. + /// </param> + /// <returns> + /// The access token. + /// </returns> + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) { + try { + var entity = + MessagingUtilities.CreateQueryString( + new Dictionary<string, string> { + { "client_id", this.appId }, + { "redirect_uri", returnUrl.AbsoluteUri }, + { "client_secret", this.appSecret }, + { "code", authorizationCode }, + { "grant_type", "authorization_code" }, + }); + + WebRequest tokenRequest = WebRequest.Create(TokenEndpoint); + tokenRequest.ContentType = "application/x-www-form-urlencoded"; + tokenRequest.ContentLength = entity.Length; + tokenRequest.Method = "POST"; + + using (Stream requestStream = tokenRequest.GetRequestStream()) { + var writer = new StreamWriter(requestStream); + writer.Write(entity); + writer.Flush(); + } + + HttpWebResponse tokenResponse = (HttpWebResponse)tokenRequest.GetResponse(); + if (tokenResponse.StatusCode == HttpStatusCode.OK) { + using (Stream responseStream = tokenResponse.GetResponseStream()) { + var tokenData = JsonHelper.Deserialize<OAuth2AccessTokenData>(responseStream); + if (tokenData != null) { + AzureADClaims claimsAD; + claimsAD = this.ParseAccessToken(tokenData.AccessToken, true); + if (claimsAD != null) { + this.tenantid = claimsAD.Tid; + this.userid = claimsAD.Oid; + return tokenData.AccessToken; + } + return string.Empty; + } + } + } + + return null; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return null; + } + } + + /// <summary> + /// Base64 decode function except that it switches -_ to +/ before base64 decode + /// </summary> + /// <param name="str"> + /// The string to be base64urldecoded. + /// </param> + /// <returns> + /// Decoded string as string using UTF8 encoding. + /// </returns> + private static string Base64URLdecode(string str) { + System.Text.UTF8Encoding encoder = new System.Text.UTF8Encoding(); + return encoder.GetString(Base64URLdecodebyte(str)); + } + + /// <summary> + /// Base64 decode function except that it switches -_ to +/ before base64 decode + /// </summary> + /// <param name="str"> + /// The string to be base64urldecoded. + /// </param> + /// <returns> + /// Decoded string as bytes. + /// </returns> + private static byte[] Base64URLdecodebyte(string str) { + // First replace chars and then pad per spec + str = str.Replace('-', '+').Replace('_', '/'); + str = str.PadRight(str.Length + ((4 - (str.Length % 4)) % 4), '='); + return Convert.FromBase64String(str); + } + + /// <summary> + /// Validate whether the unsigned value is same as signed value + /// </summary> + /// <param name="uval"> + /// The raw input of the string signed using the key + /// </param> + /// <param name="sval"> + /// The signature of the string + /// </param> + /// <param name="certthumb"> + /// The thumbprint of cert used to encrypt token + /// </param> + /// <returns> + /// True if same, false otherwise. + /// </returns> + private static bool ValidateSig(byte[] uval, byte[] sval, byte[] certthumb) { + try { + bool ret = false; + + X509Certificate2[] certx509 = GetEncodingCert(); + string certthumbhex = string.Empty; + + // Get the hexadecimail representation of the certthumbprint + for (int i = 0; i < certthumb.Length; i++) { + certthumbhex += certthumb[i].ToString("X2"); + } + + for (int c = 0; c < certx509.Length; c++) { + // Skip any cert that does not have the same thumbprint as token + if (certx509[c].Thumbprint.ToLower() != certthumbhex.ToLower()) { + continue; + } + X509SecurityToken tok = new X509SecurityToken(certx509[c]); + if (tok == null) { + return false; + } + for (int i = 0; i < tok.SecurityKeys.Count; i++) { + X509AsymmetricSecurityKey key = tok.SecurityKeys[i] as X509AsymmetricSecurityKey; + RSACryptoServiceProvider rsa = key.GetAsymmetricAlgorithm(SecurityAlgorithms.RsaSha256Signature, false) as RSACryptoServiceProvider; + + if (rsa == null) { + continue; + } + ret = rsa.VerifyData(uval, hash, sval); + if (ret == true) { + return ret; + } + } + } + return ret; + } catch (CryptographicException e) { + Console.WriteLine(e.ToStringDescriptive()); + return false; + } + } + + /// <summary> + /// Returns the certificate with which the token is encoded. + /// </summary> + /// <returns> + /// The encoding certificate. + /// </returns> + private static X509Certificate2[] GetEncodingCert() { + if (encodingcert != null) { + return encodingcert; + } + try { + // Lock for exclusive access + lock (typeof(AzureADClient)) { + XmlDocument doc = new XmlDocument(); + + WebRequest request = + WebRequest.Create(MetaDataEndpoint); + using (WebResponse response = request.GetResponse()) { + using (Stream responseStream = response.GetResponseStream()) { + doc.Load(responseStream); + XmlNodeList list = doc.GetElementsByTagName("X509Certificate"); + encodingcert = new X509Certificate2[list.Count]; + for (int i = 0; i < list.Count; i++) { + byte[] todecode_byte = Convert.FromBase64String(list[i].InnerText); + encodingcert[i] = new X509Certificate2(todecode_byte); + } + if (hash == null) { + hash = SHA256.Create(); + } + } + } + } + return encodingcert; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return null; + } + } + + /// <summary> + /// Parses the access token into an AzureAD token. + /// </summary> + /// <param name="token"> + /// The token as a string. + /// </param> + /// <param name="validate"> + /// Whether to validate against time\audience. + /// </param> + /// <returns> + /// The claims as an object and null in case of failure. + /// </returns> + private AzureADClaims ParseAccessToken(string token, bool validate) { + try { + // This is the encoded JWT token split into the 3 parts + string[] strparts = token.Split('.'); + + // Decparts has the header and claims section decoded from JWT + string jwtHeader, jwtClaims; + string jwtb64Header, jwtb64Claims, jwtb64Sig; + byte[] jwtSig; + if (strparts.Length != 3) { + return null; + } + jwtb64Header = strparts[0]; + jwtb64Claims = strparts[1]; + jwtb64Sig = strparts[2]; + jwtHeader = Base64URLdecode(jwtb64Header); + jwtClaims = Base64URLdecode(jwtb64Claims); + jwtSig = Base64URLdecodebyte(jwtb64Sig); + + JavaScriptSerializer s1 = new JavaScriptSerializer(); + + AzureADClaims claimsAD = s1.Deserialize<AzureADClaims>(jwtClaims); + AzureADHeader headerAD = s1.Deserialize<AzureADHeader>(jwtHeader); + + if (validate) { + // Check to see if the token is valid + // Check if its JWT and RSA encoded + if (headerAD.Typ.ToUpper() != "JWT") { + return null; + } + + // Check if its JWT and RSA encoded + if (headerAD.Alg.ToUpper() != "RS256") { + return null; + } + if (string.IsNullOrEmpty(headerAD.X5t)) { + return null; + } + + // Check audience to be graph + if (claimsAD.Aud.ToLower().ToLower() != GraphResource.ToLower()) { + return null; + } + + // Check issuer to be sts + if (claimsAD.Iss.ToLower().IndexOf(STSName.ToLower()) != 0) { + return null; + } + + // Check time validity + TimeSpan span = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + double secsnow = span.TotalSeconds; + double nbfsecs = Convert.ToDouble(claimsAD.Nbf); + double expsecs = Convert.ToDouble(claimsAD.Exp); + if ((nbfsecs - 100 > secsnow) || (secsnow > expsecs + 100)) { + return null; + } + + // Validate the signature of the token + string tokUnsigned = jwtb64Header + "." + jwtb64Claims; + if (!ValidateSig(Encoding.UTF8.GetBytes(tokUnsigned), jwtSig, Base64URLdecodebyte(headerAD.X5t))) { + return null; + } + } + return claimsAD; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return null; + } + } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs new file mode 100644 index 0000000..8269419 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADGraph.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.Serialization; + + /// <summary> + /// Contains data of a AzureAD user. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable in medium trust, it has to be public. + /// </remarks> + [DataContract] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + public class AzureADGraph { + #region Public Properties + + /// <summary> + /// Gets or sets the firstname. + /// </summary> + /// <value> The first name. </value> + [DataMember(Name = "givenName")] + public string GivenName { get; set; } + + /// <summary> + /// Gets or sets the lastname. + /// </summary> + /// <value> The last name. </value> + [DataMember(Name = "surname")] + public string Surname { get; set; } + + /// <summary> + /// Gets or sets the email. + /// </summary> + /// <value> The email. </value> + [DataMember(Name = "userPrincipalName")] + public string UserPrincipalName { get; set; } + + /// <summary> + /// Gets or sets the fullname. + /// </summary> + /// <value> The fullname. </value> + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value> The id. </value> + [DataMember(Name = "objectId")] + public string ObjectId { get; set; } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs new file mode 100644 index 0000000..042eccb --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADHeader.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.Serialization; + + /// <summary> + /// Contains header of AzureAD JWT token. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable in medium trust, it has to be public. + /// </remarks> + [DataContract] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + + public class AzureADHeader { + #region Public Properties + + /// <summary> + /// Gets or sets the type of token. Will always be JWT + /// </summary> + /// <value> The type of token. </value> + [DataMember(Name = "typ")] + public string Typ { get; set; } + + /// <summary> + /// Gets or sets the algo of the header. + /// </summary> + /// <value> The algo of encoding. </value> + [DataMember(Name = "alg")] + public string Alg { get; set; } + + /// <summary> + /// Gets or sets the thumbprint of the header. + /// </summary> + /// <value> The thumbprint of the cert used to encode. </value> + [DataMember(Name = "x5t")] + public string X5t { get; set; } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj index 405ac3c..ea87cfd 100644 --- a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj +++ b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj @@ -44,6 +44,10 @@ <ItemGroup> <Compile Include="AuthenticationResult.cs" /> <Compile Include="Clients\DictionaryExtensions.cs" /> + <Compile Include="Clients\OAuth2\AzureADClaims.cs" /> + <Compile Include="Clients\OAuth2\AzureADClient.cs" /> + <Compile Include="Clients\OAuth2\AzureADGraph.cs" /> + <Compile Include="Clients\OAuth2\AzureADHeader.cs" /> <Compile Include="Clients\OAuth2\WindowsLiveClient.cs" /> <Compile Include="Clients\OAuth\AuthenticationOnlyCookieOAuthTokenManager.cs"> <SubType>Code</SubType> |