summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorGirish Bablani <girishb@microsoft.com>2013-04-16 18:53:13 -0700
committerGirish Bablani <girishb@microsoft.com>2013-04-16 18:53:13 -0700
commit789f14adf18e65ab416b60341bfbecc6577a1c37 (patch)
tree3f140b5f1679cb2857cd65a7d14d2c0ec28f2fca /src
parent9e33a9e89ba1973cb3bf923e1303105047094d9c (diff)
downloadDotNetOpenAuth-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.Config6
-rw-r--r--src/.nuget/NuGet.targets153
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs77
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs510
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs61
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs51
-rw-r--r--src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj4
-rw-r--r--src/DotNetOpenAuth.sln9
-rw-r--r--src/packages/repositories.config5
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