Created
October 15, 2014 19:49
-
-
Save jmichas/46b37235ae2b6058a820 to your computer and use it in GitHub Desktop.
Azure Mobile Service Login Controller for Thinktecture IdentityServer v3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Configuration; | |
using System.Diagnostics; | |
using System.IdentityModel.Protocols.WSTrust; | |
using System.IdentityModel.Tokens; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Security.Claims; | |
using System.Security.Cryptography; | |
using System.ServiceModel.Security.Tokens; | |
using System.Text; | |
using System.Threading.Tasks; | |
using System.Web.Http; | |
using Microsoft.WindowsAzure.Mobile.Service; | |
using Microsoft.WindowsAzure.Mobile.Service.Security; | |
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
namespace Michas.MobileService.Controllers | |
{ | |
/// <summary> | |
/// This controller is responsible for receiving an auth_token from the Thinktecture IdentityServer v3 token endpoint and attempting to: | |
/// 1. Validate the token with the IDP | |
/// 2. Retrieve the userinfo for the scopes inside the auth token | |
/// 3. Creating a ZUMO auth token for use with the MobileServiceClient for azure mobile services | |
/// | |
/// The client app/device should be responsible for watching the expiration and requesting a new token using the refresh token or prompting for user credentials. | |
/// </summary> | |
[AuthorizeLevel(AuthorizationLevel.Anonymous)] | |
public class IdpLoginController : ApiController | |
{ | |
private readonly string _azureMasterKey; | |
private readonly string _tokenValidationEndpointFormat; | |
private readonly string _userInfoEndpoint; | |
public ApiServices Services { get; set; } | |
private const string ProviderName = "Custom"; //Replace with your "provider name" if you wish, might be useful if mixing with other OOB providers ie Facebook | |
public IdpLoginController() | |
{ | |
var idpUri = ConfigurationManager.AppSettings["IdPSiteUrl"]; | |
_azureMasterKey = ConfigurationManager.AppSettings["MS_MasterKey"]; | |
_tokenValidationEndpointFormat = idpUri + "/connect/accesstokenvalidation?token={0}"; | |
_userInfoEndpoint = idpUri + "/connect/userinfo"; | |
} | |
public async Task<HttpResponseMessage> Post(LoginToken token) | |
{ | |
var client = new WebClient(); | |
var tokenValid = true; | |
ValidationResponse validationResponse = null; | |
//validation token with idp | |
try | |
{ | |
var validationResponseJson = | |
client.DownloadString(new Uri(String.Format(_tokenValidationEndpointFormat, token.JwtToken))); | |
validationResponse = JsonConvert.DeserializeObject<ValidationResponse>(validationResponseJson); | |
Debug.WriteLine(validationResponse.ToString()); | |
} | |
catch (WebException exception) | |
{ | |
tokenValid = false; | |
Debug.WriteLine(exception.Message); | |
} | |
if (!tokenValid) return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Unable to validate token"); | |
//get user info from idp using token | |
var userInfoClient = new UserInfoClient(new Uri(_userInfoEndpoint), token.JwtToken); | |
var userInfoResponse = await userInfoClient.GetAsync(); | |
Debug.WriteLine(userInfoResponse.Raw); | |
//create zumo token and return to client | |
return Request.CreateResponse(HttpStatusCode.OK, | |
CreateAuthenticationResponse(validationResponse.SubjectIdentifier, userInfoResponse.Claims.ToList(), validationResponse.Expiration, _azureMasterKey)); | |
} | |
private static LoginResult CreateAuthenticationResponse(string uid, IEnumerable<Tuple<string, string>> additionalClaims, int exp, string secretKey) | |
{ | |
var userId = String.Format("{0}:{1}", ProviderName, uid); | |
var response = new LoginResult | |
{ | |
User = new LoginResultUser | |
{ | |
UserId = userId | |
}, | |
AuthenticationToken = CreateJwt(userId, exp, additionalClaims, secretKey) | |
}; | |
return response; | |
} | |
private static string CreateJwt(string uid, int exp, IEnumerable<Tuple<string, string>> additionalClaims, string secretKey) | |
{ | |
var privateKey = secretKey; | |
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); | |
var expiration = utc0.AddSeconds(exp); | |
var providerClaims = new JObject(); | |
foreach (var claim in additionalClaims) | |
{ | |
providerClaims.Add(claim.Item1, claim.Item2); | |
} | |
var claims = new List<Claim>() | |
{ | |
new Claim("urn:microsoft:credentials", providerClaims.ToString(Formatting.None)), | |
new Claim("uid", uid), | |
new Claim("ver", "2"), | |
}; | |
return JsonWebToken.CreateTokenFromClaims(claims, privateKey, "urn:microsoft:windows-azure:zumo", "urn:microsoft:windows-azure:zumo", expiration).Token.RawData; | |
} | |
} | |
public class LoginToken | |
{ | |
public string JwtToken { get; set; } | |
} | |
public class ValidationResponse | |
{ | |
[JsonProperty("client_id")] | |
public string ClientId { get; set; } | |
[JsonProperty("sub")] | |
public string SubjectIdentifier { get; set; } | |
[JsonProperty("amr")] | |
public string Amr { get; set; } | |
[JsonProperty("auth_time")] | |
public int AuthTime { get; set; } | |
[JsonProperty("idp")] | |
public string Idp { get; set; } | |
[JsonProperty("iss")] | |
public string Issuer { get; set; } | |
[JsonProperty("aud")] | |
public string Audience { get; set; } | |
[JsonProperty("exp")] | |
public int Expiration { get; set; } | |
[JsonProperty("nbf")] | |
public int NotBefore { get; set; } | |
[JsonProperty("scope")] | |
public List<string> Scopes { get; set; } | |
public override string ToString() | |
{ | |
var sb = new StringBuilder(); | |
sb.AppendLine("ClientId = " + ClientId); | |
sb.AppendLine("SubjectIdentifier = " + SubjectIdentifier); | |
sb.AppendLine("Amr = " + Amr); | |
sb.AppendLine("AuthTime = " + AuthTime); | |
sb.AppendLine("Idp = " + Idp); | |
sb.AppendLine("Issuer = " + Issuer); | |
sb.AppendLine("Audience = " + Audience); | |
sb.AppendLine("Expiration = " + Expiration); | |
sb.AppendLine("NotBefore = " + NotBefore); | |
sb.AppendLine("Scopes = " + Scopes); | |
return base.ToString(); | |
} | |
} | |
public class JsonWebToken | |
{ | |
public static TokenInfo CreateTokenFromClaims(IEnumerable<Claim> claims, string secretKey, string audience, string issuer, DateTime expiration) | |
{ | |
byte[] signingKey = GetSigningKey(secretKey); | |
BinarySecretSecurityToken signingToken = new BinarySecretSecurityToken(signingKey); | |
SigningCredentials signingCredentials = new SigningCredentials(new InMemorySymmetricSecurityKey(signingToken.GetKeyBytes()), "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", "http://www.w3.org/2001/04/xmlenc#sha256"); | |
DateTime created = DateTime.UtcNow; | |
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor | |
{ | |
AppliesToAddress = audience, | |
TokenIssuerName = issuer, | |
SigningCredentials = signingCredentials, | |
Lifetime = new Lifetime(created, expiration), | |
Subject = new ClaimsIdentity(claims), | |
}; | |
JwtSecurityTokenHandler securityTokenHandler = new JwtSecurityTokenHandler(); | |
JwtSecurityToken token = securityTokenHandler.CreateToken(tokenDescriptor) as JwtSecurityToken; | |
return new TokenInfo { Token = token }; | |
} | |
public static byte[] GetSigningKey(string secretKey) | |
{ | |
if (string.IsNullOrEmpty(secretKey)) | |
{ | |
throw new ArgumentNullException("secretKey"); | |
} | |
UTF8Encoding encoder = new UTF8Encoding(true, true); | |
byte[] computeHashInput = encoder.GetBytes(secretKey + "JWTSig"); | |
byte[] signingKey = null; | |
using (var sha256Provider = new SHA256Managed()) | |
{ | |
signingKey = sha256Provider.ComputeHash(computeHashInput); | |
} | |
return signingKey; | |
} | |
} | |
public class UserInfoResponse | |
{ | |
public UserInfoResponse(string raw) | |
{ | |
Raw = raw; | |
try | |
{ | |
JsonObject = JObject.Parse(raw); | |
var claims = new List<Tuple<string, string>>(); | |
foreach (var x in JsonObject) | |
{ | |
claims.Add(Tuple.Create(x.Key, x.Value.ToString())); | |
} | |
Claims = claims; | |
} | |
catch (Exception ex) | |
{ | |
IsError = true; | |
ErrorMessage = ex.Message; | |
} | |
} | |
public UserInfoResponse(HttpStatusCode statusCode, string httpErrorReason) | |
{ | |
IsHttpError = true; | |
HttpErrorStatusCode = statusCode; | |
HttpErrorReason = httpErrorReason; | |
} | |
public string Raw { get; private set; } | |
public JObject JsonObject { get; private set; } | |
public IEnumerable<Tuple<string, string>> Claims { get; set; } | |
public bool IsHttpError { get; private set; } | |
public HttpStatusCode HttpErrorStatusCode { get; private set; } | |
public string HttpErrorReason { get; private set; } | |
public bool IsError { get; private set; } | |
public string ErrorMessage { get; set; } | |
} | |
public class UserInfoClient | |
{ | |
private readonly HttpClient _client; | |
public UserInfoClient(Uri endpoint, string token) | |
: this(endpoint, token, new HttpClientHandler()) | |
{ } | |
public UserInfoClient(Uri endpoint, string token, HttpClientHandler innerHttpClientHandler) | |
{ | |
if (endpoint == null) | |
throw new ArgumentNullException("endpoint"); | |
if (string.IsNullOrEmpty(token)) | |
throw new ArgumentNullException("token"); | |
if (innerHttpClientHandler == null) | |
throw new ArgumentNullException("inneHttpClientHandler"); | |
_client = new HttpClient(innerHttpClientHandler) | |
{ | |
BaseAddress = endpoint | |
}; | |
_client.SetBearerToken(token); | |
} | |
public TimeSpan Timeout | |
{ | |
set | |
{ | |
_client.Timeout = value; | |
} | |
} | |
public async Task<UserInfoResponse> GetAsync() | |
{ | |
var response = await _client.GetAsync(""); | |
if (response.StatusCode != HttpStatusCode.OK) | |
return new UserInfoResponse(response.StatusCode, response.ReasonPhrase); | |
var content = await response.Content.ReadAsStringAsync(); | |
return new UserInfoResponse(content); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment