diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a11a938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates +*.tss +*.nupkg + + +# Build results +[Dd]ebug/ +[Rr]elease/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.vspscc +.builds +*.dotCover + +## TODO: If you have NuGet Package Restore enabled, uncomment this +packages/ +#!src/packages/Microsoft*/ + + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp + +# ReSharper is a .NET coding add-in +_ReSharper* + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Others +[Bb]in +[Oo]bj +sql +TestResults +*.Cache +ClientBin +stylecop.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML + + + +############ +## Windows +############ + +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +# Mac crap +.DS_Store + +!*.targets diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 0000000..67f8ea0 --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 0000000..86dd189 Binary files /dev/null and b/.nuget/NuGet.exe differ diff --git a/ClaimsFromJwt.nuspec b/ClaimsFromJwt.nuspec new file mode 100644 index 0000000..ee5515d --- /dev/null +++ b/ClaimsFromJwt.nuspec @@ -0,0 +1,23 @@ + + + + ClaimsFromJWT + 1.0.0 + Claims from JSON Web Token + Matias Woloski, AdvancedREI + auth0, advancedrei + http://opensource.org/licenses/MIT + http://auth0.com + https://secure.gravatar.com/avatar/805765c256ff8617fcad483b5476faf2?s=420 + false + A set of helper functions that translate an already-validated JWT into a ClaimsIdentity, using standard Claims wherever possible. + Copyright © 2013-2014 Auth0 and AdvancedREI, LLC. + jwt, webapi, Auth0, OAuth + + + + + + + + diff --git a/DeployNuGet.bat b/DeployNuGet.bat new file mode 100644 index 0000000..5156f38 --- /dev/null +++ b/DeployNuGet.bat @@ -0,0 +1,11 @@ +@echo off +echo Would you like to push the packages to NuGet when finished? +set /p choice="Enter y/n: " + +del *.nupkg +@echo on +".nuget/nuget.exe" pack ClaimsFromJwt.nuspec +if /i %choice% equ y ( + ".nuget/nuget.exe" push *.nupkg +) +pause \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3445ffb --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +[ClaimsFromJWT 1.0](http://github.com/AdvancedREI/ClaimsFromJWT) ================= ClaimsFromJWT is a set of helper functions to take a already-validated JWT and turn it into a ClaimsIdentity populated with as many standardized claims as possible. Quick start ----------- Install the NuGet package: `Install-Package ClaimsFromJwt`, clone the repo, `git clone git://github.com/advancedrei/claimsfromjwt.git`, or [download the latest release](https://github.com/advancedrei/claimsfromjwt/zipball/master). Once the code has been installed, you can use the helper functions as needed. Deserialize the JWT into a Dictionary and then pass that into JwtHelper.GetClaimsFromJwt(). You can add to the arrays that map names to claims, or add additional claim mapping using the same technique. If you make any changes, consider sending us a pull request so that all can take advantage. Bug tracker ----------- Have a bug? Please create an issue here on GitHub that conforms with [necolas's guidelines](https://github.com/necolas/issue-guidelines). https://github.com/AdvancedREI/ClaimsFromJWT/issues Twitter account --------------- Keep up to date on announcements and more by following AdvancedREI on Twitter, [@AdvancedREI](http://twitter.com/AdvancedREI). Blog ---- Read more detailed announcements, discussions, and more on [The AdvancedREI Dev Blog](http://advancedrei.com/blogs/development). Author ------- **Robert McLaws** + http://twitter.com/robertmclaws + http://github.com/advancedrei Copyright and license --------------------- Copyright 2013-2014 Auth0, LLC. and AdvancedREI, LLC. The MIT License (MIT) Copyright (c) 2013-2014 Auth0, LLC. and AdvancedREI, LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/content/App_Start/ClaimsFromJwt.cs.pp b/content/App_Start/ClaimsFromJwt.cs.pp new file mode 100644 index 0000000..08c0ad9 --- /dev/null +++ b/content/App_Start/ClaimsFromJwt.cs.pp @@ -0,0 +1,157 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Web.Script.Serialization; + +namespace $rootnamespace$ +{ + + /// + /// A set of helper functions for transforming an already-validated JWT into a ClaimsIdentity, using + /// standardized Claims wherever possible. + /// + public static class JwtHelper + { + + #region Constants + + private const string StringClaimValueType = "http://www.w3.org/2001/XMLSchema#string"; + + #endregion + + #region Private Members + + // NOTE: You can add to or modify these arrays as necessary. The routine will automatically remove + // any "_" characters. + + private static readonly string[] ClaimTypesForUserId = { "userid" }; + private static readonly string[] ClaimTypesForRoles = { "roles", "role" }; + private static readonly string[] ClaimTypesForEmail = { "emails", "email" }; + private static readonly string[] ClaimTypesForGivenName = { "givenname", "firstname" }; + private static readonly string[] ClaimTypesForFamilyName = { "familyname", "lastname", "surname" }; + private static readonly string[] ClaimTypesForPostalCode = { "postalcode" }; + private static readonly string[] ClaimsToExclude = { "iss", "sub", "aud", "exp", "iat", "identities" }; + + #endregion + + #region Public Methods + + /// + /// Gets a List of Claims from a given deserialized JSON token. + /// + /// The deserialized JSON payload to process. + /// The principal that issued the JWT. + /// A List of Claims derived from the JWT. + public static List GetClaimsFromJwt(Dictionary jwtData, string issuer) + { + var list = new List(); + issuer = issuer ?? DefaultIssuer; + + foreach (var pair in jwtData) + { + var claimType = GetClaimType(pair.Key); + var source = pair.Value as ArrayList; + + if (source != null) + { + // Get the claim, check to make sure it hasn't already been added. This is a workaround + // for an issue where MicrosoftAccounts return the same e-mail address twice. + foreach (var innerClaim in source.Cast().Select(item => new Claim(claimType, item.ToString(), StringClaimValueType, issuer, issuer)) + .Where(innerClaim => !list.Any(c => c.Type == innerClaim.Type && c.Value == innerClaim.Value))) + { + list.Add(innerClaim); + } + + continue; + } + + var claim = new Claim(claimType, pair.Value.ToString(), StringClaimValueType, issuer, issuer); + if (!list.Contains(claim)) + { + list.Add(claim); + } + } + + // dont include specific jwt claims + return list.Where(c => ClaimsToExclude.All(t => t != c.Type)).ToList(); + } + + /// + /// Gets a properly populated with the claims from the JWT. + /// + /// The list of claims that we've already processed. + /// The principal that issued the JWT. + /// + public static ClaimsIdentity GetClaimsIdentity(List claims, string issuer) + { + var subject = new ClaimsIdentity("Federation", ClaimTypes.Name, ClaimTypes.Role); + + foreach (var claim in claims) + { + var type = claim.Type; + if (type == ClaimTypes.Actor) + { + if (subject.Actor != null) + { + throw new InvalidOperationException(string.Format( + "Jwt10401: Only a single 'Actor' is supported. Found second claim of type: '{0}', value: '{1}'", new object[] { "actor", claim.Value })); + } + } + + var claim3 = new Claim(type, claim.Value, claim.ValueType, issuer, issuer, subject); + subject.AddClaim(claim3); + } + + return subject; + } + + /// + /// Attempts to map names from the JWT into standard Claim types. + /// + /// The name of the Claim as passed in my the JWT. + /// A string that hopefully contains the standard namespace for a given Claim. + public static string GetClaimType(string name) + { + var newName = name.Replace("_", "").ToLower(); + if (newName == "name") + { + return ClaimTypes.Name; + } + if (ClaimTypesForUserId.Contains(newName)) + { + return ClaimTypes.NameIdentifier; + } + if (ClaimTypesForRoles.Contains(newName)) + { + return ClaimTypes.Role; + } + if (ClaimTypesForEmail.Contains(newName)) + { + return ClaimTypes.Email; + } + if (ClaimTypesForGivenName.Contains(newName)) + { + return ClaimTypes.GivenName; + } + if (ClaimTypesForFamilyName.Contains(newName)) + { + return ClaimTypes.Surname; + } + if (ClaimTypesForPostalCode.Contains(newName)) + { + return ClaimTypes.PostalCode; + } + if (name == "gender") + { + return ClaimTypes.Gender; + } + + return name; + } + + #endregion + + } +}