diff options
author | Girish Bablani <girishb@microsoft.com> | 2013-04-16 18:53:13 -0700 |
---|---|---|
committer | Girish Bablani <girishb@microsoft.com> | 2013-04-16 18:53:13 -0700 |
commit | 789f14adf18e65ab416b60341bfbecc6577a1c37 (patch) | |
tree | 3f140b5f1679cb2857cd65a7d14d2c0ec28f2fca /src | |
parent | 9e33a9e89ba1973cb3bf923e1303105047094d9c (diff) | |
download | DotNetOpenAuth-789f14adf18e65ab416b60341bfbecc6577a1c37.zip DotNetOpenAuth-789f14adf18e65ab416b60341bfbecc6577a1c37.tar.gz DotNetOpenAuth-789f14adf18e65ab416b60341bfbecc6577a1c37.tar.bz2 |
Enabled AzureAD integration and added TestAzureAD sample app
Diffstat (limited to 'src')
-rw-r--r-- | src/.nuget/NuGet.Config | 6 | ||||
-rw-r--r-- | src/.nuget/NuGet.targets | 153 | ||||
-rw-r--r-- | src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs | 77 | ||||
-rw-r--r-- | src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs | 510 | ||||
-rw-r--r-- | src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs | 61 | ||||
-rw-r--r-- | src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs | 51 | ||||
-rw-r--r-- | src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj | 4 | ||||
-rw-r--r-- | src/DotNetOpenAuth.sln | 9 | ||||
-rw-r--r-- | src/packages/repositories.config | 5 |
9 files changed, 876 insertions, 0 deletions
diff --git a/src/.nuget/NuGet.Config b/src/.nuget/NuGet.Config new file mode 100644 index 0000000..67f8ea0 --- /dev/null +++ b/src/.nuget/NuGet.Config @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <solution> + <add key="disableSourceControlIntegration" value="true" /> + </solution> +</configuration>
\ No newline at end of file diff --git a/src/.nuget/NuGet.targets b/src/.nuget/NuGet.targets new file mode 100644 index 0000000..d3befda --- /dev/null +++ b/src/.nuget/NuGet.targets @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir> + + <!-- Enable the restore command to run before builds --> + <RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages> + + <!-- Property that enables building a package from a project --> + <BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage> + + <!-- Determines if package restore consent is required to restore packages --> + <RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent> + + <!-- Download NuGet.exe if it does not already exist --> + <DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe> + </PropertyGroup> + + <ItemGroup Condition=" '$(PackageSources)' == '' "> + <!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used --> + <!-- The official NuGet package source (https://nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list --> + <!-- + <PackageSource Include="https://nuget.org/api/v2/" /> + <PackageSource Include="https://my-nuget-source/nuget/" /> + --> + </ItemGroup> + + <PropertyGroup Condition=" '$(OS)' == 'Windows_NT'"> + <!-- Windows specific commands --> + <NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath> + <PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig> + <PackagesDir>$([System.IO.Path]::Combine($(SolutionDir), "packages"))</PackagesDir> + </PropertyGroup> + + <PropertyGroup Condition=" '$(OS)' != 'Windows_NT'"> + <!-- We need to launch nuget.exe with the mono command if we're not on windows --> + <NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath> + <PackagesConfig>packages.config</PackagesConfig> + <PackagesDir>$(SolutionDir)packages</PackagesDir> + </PropertyGroup> + + <PropertyGroup> + <!-- NuGet command --> + <NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\nuget.exe</NuGetExePath> + <PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources> + + <NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand> + <NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand> + + <PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir> + + <RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch> + <!-- Commands --> + <RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -o "$(PackagesDir)"</RestoreCommand> + <BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols</BuildCommand> + + <!-- We need to ensure packages are restored prior to assembly resolve --> + <ResolveReferencesDependsOn Condition="$(RestorePackages) == 'true'"> + RestorePackages; + $(ResolveReferencesDependsOn); + </ResolveReferencesDependsOn> + + <!-- Make the build depend on restore packages --> + <BuildDependsOn Condition="$(BuildPackage) == 'true'"> + $(BuildDependsOn); + BuildPackage; + </BuildDependsOn> + </PropertyGroup> + + <Target Name="CheckPrerequisites"> + <!-- Raise an error if we're unable to locate nuget.exe --> + <Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" /> + <SetEnvironmentVariable EnvKey="VisualStudioVersion" EnvValue="$(VisualStudioVersion)" Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' " /> + <!-- + Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once. + This effectively acts as a lock that makes sure that the download operation will only happen once and all + parallel builds will have to wait for it to complete. + --> + <MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT" /> + </Target> + + <Target Name="_DownloadNuGet"> + <DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" /> + </Target> + + <Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites"> + <Exec Command="$(RestoreCommand)" + Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" /> + + <Exec Command="$(RestoreCommand)" + LogStandardErrorAsError="true" + Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" /> + </Target> + + <Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites"> + <Exec Command="$(BuildCommand)" + Condition=" '$(OS)' != 'Windows_NT' " /> + + <Exec Command="$(BuildCommand)" + LogStandardErrorAsError="true" + Condition=" '$(OS)' == 'Windows_NT' " /> + </Target> + + <UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll"> + <ParameterGroup> + <OutputFilename ParameterType="System.String" Required="true" /> + </ParameterGroup> + <Task> + <Reference Include="System.Core" /> + <Using Namespace="System" /> + <Using Namespace="System.IO" /> + <Using Namespace="System.Net" /> + <Using Namespace="Microsoft.Build.Framework" /> + <Using Namespace="Microsoft.Build.Utilities" /> + <Code Type="Fragment" Language="cs"> + <![CDATA[ + try { + OutputFilename = Path.GetFullPath(OutputFilename); + + Log.LogMessage("Downloading latest version of NuGet.exe..."); + WebClient webClient = new WebClient(); + webClient.DownloadFile("https://nuget.org/nuget.exe", OutputFilename); + + return true; + } + catch (Exception ex) { + Log.LogErrorFromException(ex); + return false; + } + ]]> + </Code> + </Task> + </UsingTask> + + <UsingTask TaskName="SetEnvironmentVariable" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll"> + <ParameterGroup> + <EnvKey ParameterType="System.String" Required="true" /> + <EnvValue ParameterType="System.String" Required="true" /> + </ParameterGroup> + <Task> + <Using Namespace="System" /> + <Code Type="Fragment" Language="cs"> + <![CDATA[ + try { + Environment.SetEnvironmentVariable(EnvKey, EnvValue, System.EnvironmentVariableTarget.Process); + } + catch { + } + ]]> + </Code> + </Task> + </UsingTask> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs new file mode 100644 index 0000000..eda649c --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------- +// <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..578975c --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs @@ -0,0 +1,510 @@ +//----------------------------------------------------------------------- +// <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..7632900 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// <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> diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index 8a44fd7..babe701 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -219,6 +219,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.OAuth.Common EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.OAuth2.ClientAuthorization", "DotNetOpenAuth.OAuth2.ClientAuthorization\DotNetOpenAuth.OAuth2.ClientAuthorization.csproj", "{CCF3728A-B3D7-404A-9BC6-75197135F2D7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAzureAD", "..\samples\TestAzureAD\TestAzureAD.csproj", "{C62A052B-7914-4511-942A-A3F4609AC77A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeAnalysis|Any CPU = CodeAnalysis|Any CPU @@ -503,6 +505,12 @@ Global {CCF3728A-B3D7-404A-9BC6-75197135F2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCF3728A-B3D7-404A-9BC6-75197135F2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCF3728A-B3D7-404A-9BC6-75197135F2D7}.Release|Any CPU.Build.0 = Release|Any CPU + {C62A052B-7914-4511-942A-A3F4609AC77A}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {C62A052B-7914-4511-942A-A3F4609AC77A}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {C62A052B-7914-4511-942A-A3F4609AC77A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C62A052B-7914-4511-942A-A3F4609AC77A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C62A052B-7914-4511-942A-A3F4609AC77A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C62A052B-7914-4511-942A-A3F4609AC77A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -514,6 +522,7 @@ Global {8A5CEDB9-7F8A-4BE2-A1B9-97130F453277} = {B4C6F647-C046-4B54-BE12-7701C4119EE7} {2DA24D4F-6918-43CF-973C-BC9D818F8E90} = {B4C6F647-C046-4B54-BE12-7701C4119EE7} {AA78D112-D889-414B-A7D4-467B34C7B663} = {B4C6F647-C046-4B54-BE12-7701C4119EE7} + {C62A052B-7914-4511-942A-A3F4609AC77A} = {B4C6F647-C046-4B54-BE12-7701C4119EE7} {2A59DE0A-B76A-4B42-9A33-04D34548353D} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} {AEA29D4D-396F-47F6-BC81-B58D4B855245} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} {07B193F1-68AD-4E9C-98AF-BEFB5E9403CB} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} diff --git a/src/packages/repositories.config b/src/packages/repositories.config new file mode 100644 index 0000000..a3c768a --- /dev/null +++ b/src/packages/repositories.config @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<repositories> + <repository path="..\..\samples\TestAzureAD\packages.config" /> + <repository path="..\TestAD\packages.config" /> +</repositories>
\ No newline at end of file |