locked
Owin equivalent to LiveAuthClient.GetUserId RRS feed

  • Question

  • User301348547 posted

    Hello,

    I am trying to do SSO across a Windows 8 App and an ASP.NET page, but I would like to do it in a cleaner way than what I see in this sample.

    TIn the sample, when the Windows 8 App wants to tell the site "look, do that for the connected user - the one using this Microsoft account", it sends an authorisation token. Then, with a call to LiveSDK, the site gets the userId.

    The problem with this exaple is that it is not ready for production use. The ASP.NET User is not necessarily up to date and ther [token to User] mapping is done inside the Controller Action, not at a global level.

    I would like this to be a bit more consistent and integrated: whenever there's a request to the site, containing this token, try to authenticate the user. I think I understood what I need to do, but I still have questions:

    1) The request send by the Metro app should send an Authorisation Header with Bearer scheme and a token.

    2) On the server, I should add a new Filter in the WebApiConfig (is it necessary?).

    This filter should use LiveAuthClient.GetUserId (isn't there any alternative?) to get a UserId from the token, and based on that UserId I can create a new user (if not existing) or just sign in the user.

     (My site is mostly WebAPI SPA, but the requests I mostly want to add security on are towards SignalR hubs).

    I have activated Microsoft Account (using the same app clientId as the Windows store app) but when:

    1) I open the site and connect to it with my Microsoft Account

    2) then call an authorized resource from my Windows 8 App (either a normal GET call or an attempt to start a SignalR Hub with the Authorize attribute)

    > I get redirected to the Login page. There's no SSO, despite the fact that I'm connected in both places with the same microsoft account.

    I can alway allow anonymous calls to my Hub and use LiveSDK to get the UserId whenever there's a call to the GameHub, but I am looking for better alternatives.

    Thursday, May 22, 2014 5:45 AM

Answers

  • User301348547 posted

    The first one could be useful, but I wanted a quite different approach, without redirects (as one part of the equation is actually an app, not a site).

    After a lot of source code browsing, some looking up into documentation and a lot of tests I ended up with an acceptable solution, which allows me to do the following in Startup.Auth.cs:

    var msClientId = ConfigurationManager.AppSettings["MicrosoftClientId"];
    var msClientSecret = ConfigurationManager.AppSettings["MicrosoftSecret"];
    
    app.UseMicrosoftAccountAuthentication(msClientId, msClientSecret)
       .UseLiveSso(msClientId, msClientSecret, autoCreateUser: true);

    First we need some classes and extensions to pass parameters around. Most of them require at least using Microsoft.Owin.Security;

    A class to contain the options (this is the simplest way to write it. Ideally it should be written with readonly properties c# vNext-like, and with validation for nulls).

    public class LiveSsoOptions : AuthenticationOptions
    {
        public LiveSsoOptions() : base("LiveSso")
        { }
    
        public bool AutoCreateUser { get; set; }
        public string ClientSecret { get; set; }
        public string ClientId { get; set; }
    }

    Then a class to contain the extensions, to call our new authentication middleware:

    public static class LiveSsoExtensions
    {
        public static IAppBuilder UseLiveSso(this IAppBuilder app, LiveSsoOptions options)
        {
            if (app == null)
                throw new ArgumentNullException("app");
            app.Use(typeof(LiveSsoMiddleware), app, options);
            app.UseStageMarker(PipelineStage.Authenticate);
            return app;
        }
    
        public static IAppBuilder UseLiveSso(this IAppBuilder app, string clientId, string clientSecret, bool autoCreateUser = false)
        {
            return UseLiveSso(app, new LiveSsoOptions { ClientId = clientId, ClientSecret = clientSecret, AutoCreateUser = autoCreateUser });
        }
    }

    Then the middleware. This is a quite stupid class, just passing parameters around and initialising a logger.

    internal class LiveSsoMiddleware : AuthenticationMiddleware<LiveSsoOptions>
    {
        private readonly ILogger _logger;
    
        public LiveSsoMiddleware(OwinMiddleware next, IAppBuilder app, LiveSsoOptions options)
        :base(next, options)
        {
            _logger = app.CreateLogger<LiveSsoMiddleware>();
        }
    
        protected override AuthenticationHandler<LiveSsoOptions> CreateHandler()
        {
            return new LiveSsoHandler(_logger);
        }
    }

    And finally the class that does the real work, and is initialised 1/request:

    internal class LiveSsoHandler : AuthenticationHandler<LiveSsoOptions>
        {
            private readonly ILogger _logger;
            private LiveAuthClient _authClient;
    
            public LiveSsoHandler(ILogger logger)
            {
                if (logger == null)
                    throw new ArgumentNullException("logger");
    
                _logger = logger;
            }
    
            protected async override Task InitializeCoreAsync()
            {
                _authClient = new LiveAuthClient(Options.ClientId,
                                                Options.ClientSecret,
                                                Request.Scheme + "://" + Request.Uri.Host + "/signin-microsoft");
                await base.InitializeCoreAsync();
            }
    
            protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
            {
                string[] authenticationTokens;
                if (Request.Headers.TryGetValue("LiveAuthentication", out authenticationTokens))
                {
                    var userId = _authClient.GetUserId(authenticationTokens.First());
                    if (userId == null)
                        return null;
    
                    var userManager = Context.GetUserManager<ApplicationUserManager>();
                    var user = await GetOrCreateUser(userManager, userId, Request);
                    if (user == null)
                        return null;
    
                    var identity = await userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ExternalBearer);//to see. do they sign in? Authorize works?
                    var now = DateTime.UtcNow;
                    var properties = new AuthenticationProperties { IsPersistent = true /*optimistic*/, IssuedUtc = now, ExpiresUtc = now.AddDays(1) };
                    //for better performance, call to authorize later?
                    return new AuthenticationTicket(identity, properties);
                }
                else
                    return null;
            }
    
            private async Task<ApplicationUser> GetOrCreateUser(ApplicationUserManager userManager, string userId, Microsoft.Owin.IOwinRequest Request)
            {
                var user = await userManager.FindByIdAsync(userId);
                if (user != null)
                    return user;
    
                var bearerToken = Request.Headers["Authorization"].Substring(7);
                var httpClient = new HttpClient();
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
                var result = await httpClient.GetAsync(new Uri("https://apis.live.net/v5.0/me"));
                if (!result.IsSuccessStatusCode)
                {
                    _logger.WriteWarning("Unable to get personal Information for userId {0}", userId);
                    return null;
                }
    
                string userName = (await result.Content.ReadAsAsync<LiveProfile>()).Name;
                user = await userManager.FindByNameAsync(userName);
                if (user != null)
                    return user;
                else if (Options.AutoCreateUser)
                    return await CreateUser(userId, userName, userManager);
                else
                    return null;
            }
    }

    LiveProfile is a simple POCO to convert the Json response from Live to something typed. I used Visual Studio's "Paste JSON As Classes" option to generate it:

    public class LiveProfile
    {
        public string Id { get; set; }
        public string Name { get; set; }
    /*   public string first_name { get; set; }
        public string last_name { get; set; }
        public string link { get; set; }
        public object gender { get; set; 
        public string locale { get; set; }
        public DateTime updated_time { get; set; }*/
    }

    On the client side, there are two headers to set:

    var authClient = new LiveAuthClient(redirectUrl);
    
    var loginResult = await authClient.LoginAsync(new string[] { "wl.signin" , "wl.basic" /*, "wl.emails"*/ });
    if (loginResult.Status == LiveConnectSessionStatus.Connected)
    {
        var session = loginResult.Session;
        var testHttp = new HttpClient();
        testHttp.DefaultRequestHeaders.Authorization = new HttpCredentialsHeaderValue("Bearer", session.AccessToken);
        testHttp.DefaultRequestHeaders.Add("LiveAuthentication", session.AuthenticationToken);
    ...

    The AuthenticationToken allows me to identify a user (get his/her UserId). This magic happens because both App and Site share the same ClientId and ClientSecret.

    In my case, I decided to also create the user if (s)he doesn't exist on the site, using the same UserId as the Microsoft one (relatively safe, as this is the primary way to create users on my site). That's why I also share the AccessToken, which I use to get user's profile data if needed. I opted for not requesting the user's email, thus reducing risks to users' privacy to a minimum.

    Please mind that the way I decided to create users automatically creates a lot of dependencies and assumptions that you might want to avoid by implementing some details differently.

    It can probably get better if I sign in (performance-wise, avoiding checking for the user at each request, for example). Any suggestions are welcome.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Sunday, June 1, 2014 7:00 AM

All replies

  • User301348547 posted

    Please, anyone, I would really like to know if I'm not doing anything stupid security-wise by creating my own IAuthenticationFilter and automatically mapping (and even creating) the user based on the authentication token sent by the companion app, completely bypassing the rest of the OAuth pipeline for this request.

    I also don't like creating a dependency to the LiveSDK if Microsoft.Owin.Security.MicrosoftAccount can do the work.

    Saturday, May 24, 2014 7:16 AM
  • User301348547 posted

    The answer seems not to be  the IAuthenticafionFilter, it is not called when I thought it would be called (not on every not yet authenticated request).

    Saturday, May 24, 2014 7:33 AM
  • User301348547 posted

    The first one could be useful, but I wanted a quite different approach, without redirects (as one part of the equation is actually an app, not a site).

    After a lot of source code browsing, some looking up into documentation and a lot of tests I ended up with an acceptable solution, which allows me to do the following in Startup.Auth.cs:

    var msClientId = ConfigurationManager.AppSettings["MicrosoftClientId"];
    var msClientSecret = ConfigurationManager.AppSettings["MicrosoftSecret"];
    
    app.UseMicrosoftAccountAuthentication(msClientId, msClientSecret)
       .UseLiveSso(msClientId, msClientSecret, autoCreateUser: true);

    First we need some classes and extensions to pass parameters around. Most of them require at least using Microsoft.Owin.Security;

    A class to contain the options (this is the simplest way to write it. Ideally it should be written with readonly properties c# vNext-like, and with validation for nulls).

    public class LiveSsoOptions : AuthenticationOptions
    {
        public LiveSsoOptions() : base("LiveSso")
        { }
    
        public bool AutoCreateUser { get; set; }
        public string ClientSecret { get; set; }
        public string ClientId { get; set; }
    }

    Then a class to contain the extensions, to call our new authentication middleware:

    public static class LiveSsoExtensions
    {
        public static IAppBuilder UseLiveSso(this IAppBuilder app, LiveSsoOptions options)
        {
            if (app == null)
                throw new ArgumentNullException("app");
            app.Use(typeof(LiveSsoMiddleware), app, options);
            app.UseStageMarker(PipelineStage.Authenticate);
            return app;
        }
    
        public static IAppBuilder UseLiveSso(this IAppBuilder app, string clientId, string clientSecret, bool autoCreateUser = false)
        {
            return UseLiveSso(app, new LiveSsoOptions { ClientId = clientId, ClientSecret = clientSecret, AutoCreateUser = autoCreateUser });
        }
    }

    Then the middleware. This is a quite stupid class, just passing parameters around and initialising a logger.

    internal class LiveSsoMiddleware : AuthenticationMiddleware<LiveSsoOptions>
    {
        private readonly ILogger _logger;
    
        public LiveSsoMiddleware(OwinMiddleware next, IAppBuilder app, LiveSsoOptions options)
        :base(next, options)
        {
            _logger = app.CreateLogger<LiveSsoMiddleware>();
        }
    
        protected override AuthenticationHandler<LiveSsoOptions> CreateHandler()
        {
            return new LiveSsoHandler(_logger);
        }
    }

    And finally the class that does the real work, and is initialised 1/request:

    internal class LiveSsoHandler : AuthenticationHandler<LiveSsoOptions>
        {
            private readonly ILogger _logger;
            private LiveAuthClient _authClient;
    
            public LiveSsoHandler(ILogger logger)
            {
                if (logger == null)
                    throw new ArgumentNullException("logger");
    
                _logger = logger;
            }
    
            protected async override Task InitializeCoreAsync()
            {
                _authClient = new LiveAuthClient(Options.ClientId,
                                                Options.ClientSecret,
                                                Request.Scheme + "://" + Request.Uri.Host + "/signin-microsoft");
                await base.InitializeCoreAsync();
            }
    
            protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
            {
                string[] authenticationTokens;
                if (Request.Headers.TryGetValue("LiveAuthentication", out authenticationTokens))
                {
                    var userId = _authClient.GetUserId(authenticationTokens.First());
                    if (userId == null)
                        return null;
    
                    var userManager = Context.GetUserManager<ApplicationUserManager>();
                    var user = await GetOrCreateUser(userManager, userId, Request);
                    if (user == null)
                        return null;
    
                    var identity = await userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ExternalBearer);//to see. do they sign in? Authorize works?
                    var now = DateTime.UtcNow;
                    var properties = new AuthenticationProperties { IsPersistent = true /*optimistic*/, IssuedUtc = now, ExpiresUtc = now.AddDays(1) };
                    //for better performance, call to authorize later?
                    return new AuthenticationTicket(identity, properties);
                }
                else
                    return null;
            }
    
            private async Task<ApplicationUser> GetOrCreateUser(ApplicationUserManager userManager, string userId, Microsoft.Owin.IOwinRequest Request)
            {
                var user = await userManager.FindByIdAsync(userId);
                if (user != null)
                    return user;
    
                var bearerToken = Request.Headers["Authorization"].Substring(7);
                var httpClient = new HttpClient();
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
                var result = await httpClient.GetAsync(new Uri("https://apis.live.net/v5.0/me"));
                if (!result.IsSuccessStatusCode)
                {
                    _logger.WriteWarning("Unable to get personal Information for userId {0}", userId);
                    return null;
                }
    
                string userName = (await result.Content.ReadAsAsync<LiveProfile>()).Name;
                user = await userManager.FindByNameAsync(userName);
                if (user != null)
                    return user;
                else if (Options.AutoCreateUser)
                    return await CreateUser(userId, userName, userManager);
                else
                    return null;
            }
    }

    LiveProfile is a simple POCO to convert the Json response from Live to something typed. I used Visual Studio's "Paste JSON As Classes" option to generate it:

    public class LiveProfile
    {
        public string Id { get; set; }
        public string Name { get; set; }
    /*   public string first_name { get; set; }
        public string last_name { get; set; }
        public string link { get; set; }
        public object gender { get; set; 
        public string locale { get; set; }
        public DateTime updated_time { get; set; }*/
    }

    On the client side, there are two headers to set:

    var authClient = new LiveAuthClient(redirectUrl);
    
    var loginResult = await authClient.LoginAsync(new string[] { "wl.signin" , "wl.basic" /*, "wl.emails"*/ });
    if (loginResult.Status == LiveConnectSessionStatus.Connected)
    {
        var session = loginResult.Session;
        var testHttp = new HttpClient();
        testHttp.DefaultRequestHeaders.Authorization = new HttpCredentialsHeaderValue("Bearer", session.AccessToken);
        testHttp.DefaultRequestHeaders.Add("LiveAuthentication", session.AuthenticationToken);
    ...

    The AuthenticationToken allows me to identify a user (get his/her UserId). This magic happens because both App and Site share the same ClientId and ClientSecret.

    In my case, I decided to also create the user if (s)he doesn't exist on the site, using the same UserId as the Microsoft one (relatively safe, as this is the primary way to create users on my site). That's why I also share the AccessToken, which I use to get user's profile data if needed. I opted for not requesting the user's email, thus reducing risks to users' privacy to a minimum.

    Please mind that the way I decided to create users automatically creates a lot of dependencies and assumptions that you might want to avoid by implementing some details differently.

    It can probably get better if I sign in (performance-wise, avoiding checking for the user at each request, for example). Any suggestions are welcome.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Sunday, June 1, 2014 7:00 AM