Skip to content

Instantly share code, notes, and snippets.

@jmichas
Created October 15, 2014 19:49
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmichas/46b37235ae2b6058a820 to your computer and use it in GitHub Desktop.
Save jmichas/46b37235ae2b6058a820 to your computer and use it in GitHub Desktop.
Azure Mobile Service Login Controller for Thinktecture IdentityServer v3
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