aspnet / Security

[Archived] Middleware for security and authorization of web apps. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
1.27k stars 600 forks source link

.net core api authentication using ws-federation #1918

Closed MSAppsDev closed 5 years ago

MSAppsDev commented 5 years ago

My .net core api is calling on-Prem ADFS (WS-Federation sign-in protocol) for authentication but context.User is always null after successful login. Below is my Startup.cs

ConfigureServices()
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = Configuration["wsfed:realm"];
options.MetadataAddress = Configuration["wsfed:metadata"];
options.SkipUnrecognizedRequests = true;
options.RequireHttpsMetadata = false;
})
.AddCookie();
}

Configure()
{
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();

        app.Use(async (context, next) =>
        {
            var user = context.User;
            if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
            {
                await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme);
                return;
            }
            else
            {
                await next();
            }
        });
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller}/{action=Index}/{id?}");
        });
}

I see that IDP is redirecting to my api and returning wresult with the token value but when i try to access context.User, it is null.

Any pointers on whats wrong here?

Tratcher commented 5 years ago

Can you share the Fiddler trace file?

Note WsFed is not appropriate for most API usage, JwtBearer is much better here.

rasitha1 commented 5 years ago

@MSAppsDev fyi your code sample works just fine for me on ADFS 2016.

MSAppsDev commented 5 years ago

@rasitha1, i am trying it on adfs 2012.

MSAppsDev commented 5 years ago

unfortunately i cannot use ad fs 2016, our Ops team setup ws-federation sign-in emitting jwt on ad fs 2012. Here is the response i am getting after entering credentials in login page.

2018-11-26T18:41:58.232Z 2018-11-26T20:41:58.232Z urn:adfs:federation:XXXX:Test ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJamN5Y0ZJMVMzZ3hTamxaYTBaRFRUTmlNbkpKT1ZGd04xbFVaeUo5LmV5SmhkV1FpT2lKMWNtNDZZV1JtY3pwbVpXUmxjbUYwYVc5dU9rRlVWRk02VkdWemRDSXNJbWx6Y3lJNkltaDBkSEE2THk5emMyOXNiMmRwYmkxemRHY3VjSFZpYkdsNExtTnZiUzloWkdaekwzTmxjblpwWTJWekwzUnlkWE4wSWl3aWFXRjBJam94TlRRek1qVTNOekU0TENKbGVIQWlPakUxTkRNeU5qUTVNVGdzSW5kcGJtRmpZMjkxYm5SdVlXMWxJam9pVUZWQ1RFbFlRVkJRVkZOVVhGeDVZWFIwYzNOd2N5SXNJblZ3YmlJNklubGhkSFJ6YzNCelFIQjFZbXhwZUdGd2NIUnpkQzVqYjIwaUxDSm5hWFpsYmw5dVlXMWxJam9pV1VGVVZGTlRVRk1pTENKbGJYQnNiM2xsWlVsRUlqb2lXVUZVVkZOVFVGTWlMQ0p6ZFdJaU9pSjVZWFIwYzNOd2MwQndkV0pzYVhoaGNIQjBjM1F1WTI5dElpd2ljbTlzWlNJNklsQnliMlJUZFhCd2IzSjBJaXdpWVhWMGFHMWxkR2h2WkNJNkluVnlianB2WVhOcGN6cHVZVzFsY3pwMFl6cFRRVTFNT2pJdU1EcGhZenBqYkdGemMyVnpPbEJoYzNOM2IzSmtVSEp2ZEdWamRHVmtWSEpoYm5Od2IzSjBJaXdpWVhWMGFGOTBhVzFsSWpvaU1qQXhPQzB4TVMweU5sUXhPRG8wTVRvMU5pNDNOREZhSWl3aWRtVnlJam9pTVM0d0luMC5ySk9WSnFlM3V1V2FtV0p6bk5sNVFfbGRhMnc2bGRtUUozZnNfRk9wM1dDSnpRVlVzbTdOSGR6T3dQZU5lWlFya3czajZDdGktVjlEbk1fT2JncEQxRUh1YXlTZ0R5TzZ5RU8xT0JDZnp4UUd1enRBNjNZVlFWd0czWnJvd3BnanV5WVBBVTZoMXMxZ1hnd2dBbUhtNTcweE91ZFk0Rzhxb1BIa08wZG95SHlUeUl6R2dZR0VhMFYxX0NYU0piV1RyWDBZMHduSmZLbEQzQmVTX1RRY1JMbUpvMktFbzhQYnBVV2g3bDlqdE9jNUplV0pKUUNjRldlOEUwOVltUjNITHJzMllvVUZJYllaWEFwZ2J6d3NXSWlMTFRLLXNBLUQtZzBQZFN2N2ozNjFVUDVBZWY5SEhQelRuWVJTeGYwRjluSUNHUm9kVUVYbjRWbUg1MGxkVkE= urn:ietf:params:oauth:token-type:jwt http://schemas.xmlsoap.org/ws/2005/02/trust/Issue http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey Since claims are null, it gets into loop for 6 to 8 (whatever is configured in ad fs retry) then errors out.
rasitha1 commented 5 years ago

Looks like WsFederationOptions is initialized with Saml and JWT token handlers however that's the wrong JWT token handler for this RequestedSecurityToken.

JwtSecurityTokenHandler expects a plain JWT string and not this BinarySecurityToken.

So in order to get this working you need to change ADFS (-EnableJWT $false) and get a regular SAML assertion. Then the sign-in should work.

Does anyone know what's the point in having JwtSecurityTokenHandler listed in WsFederationOptions? Is there ever a case where a WS-Fed RSTR can come with RequestedSecurityToken having a plain JWT?

It seems like a wrapper WsFedJwtSecurityTokenHandler is needed for handling urn:ietf:params:oauth:token-type:jwt tokens which will extract the base64 JWT from the BinarySecurityToken and then call the existing JwtSecurityTokenHandler passing in the actual JWT.

Tratcher commented 5 years ago

@brentschmaltz

MSAppsDev commented 5 years ago

Finally made it work, here is the custom tokenvalidator if someone needs, this parses BinarySecurityToken from Base 64.

using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace XXXXX.Web
{
    /// <summary>
    /// Custom JWT security token validator
    /// </summary>
    public class WsFedTokenValidator : ISecurityTokenValidator
    {
        private string _jwtTokenString = null;
        private JwtSecurityToken _jwt = null;
        private JwtSecurityTokenHandler _jwtHandler = new JwtSecurityTokenHandler();

        /// <summary>
        /// Returns true if a token can be validated
        /// </summary>
        /// <returns>true if a token can be validated</returns>
        public bool CanValidateToken => throw new NotImplementedException();

        /// <summary>
        /// Gets and sets the maximum size in bytes that will be processed.
        /// </summary>
        /// <returns>token size</returns>
        public int MaximumTokenSizeInBytes { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

        /// <summary>
        /// Read JWT token 
        /// </summary>
        /// <param name="securityToken"></param>
        /// <returns>tue in case of valid token else false</returns>
        public bool CanReadToken(string securityToken)
        {
            bool response = false;
            try
            {
                if (!string.IsNullOrWhiteSpace(securityToken))
                {
                    var xmlDoc = new System.Xml.XmlDocument();
                    xmlDoc.LoadXml(securityToken);
                    if (xmlDoc != null)
                    {
                        byte[] tokenBytes = Convert.FromBase64String(xmlDoc.InnerText); //convert the Base64-encoded binary to bytes
                        _jwtTokenString = Encoding.UTF8.GetString(tokenBytes); //get the original base64-encoded token
                        if (!string.IsNullOrWhiteSpace(_jwtTokenString))
                        {
                            _jwt = _jwtHandler.ReadToken(_jwtTokenString) as JwtSecurityToken;
                            if (_jwt != null)
                            {
                                response = true;
                            }
                        }
                    }
                }
                return response;
            }
            finally
            {

            }
        }

        /// <summary>
        /// Custom security token validation
        /// </summary>
        /// <param name="securityToken"></param>
        /// <param name="validationParameters"></param>
        /// <param name="validatedToken"></param>
        /// <returns>claims</returns>
        public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
        {
            ClaimsPrincipal principal = null;
            SecurityToken token = null;
            try
            {
                if (_jwt != null)
                {
                    principal = _jwtHandler.ValidateToken(_jwtTokenString, validationParameters, out token);
                }
                validatedToken = token;
                return principal;
            }
            finally
            {

            }
        }
    }
}