IdentityServer / IdentityServer3

OpenID Connect Provider and OAuth 2.0 Authorization Server Framework for ASP.NET 4.x/Katana
https://identityserver.github.io/Documentation/
Apache License 2.0
2.01k stars 762 forks source link

Login form on client #2061

Closed pbartos closed 9 years ago

pbartos commented 9 years ago

Hi,

I'm solving business requirement that they don't want to have login form on IdS but on MVC client (popup login, CMS, localizations, etc) and still use SSO. So I let user insert credentials on MVC client and pass it over the acr_values (encrypted) to the server with Implicit flow and use AuthenticateLocalAsync to login user. Of course, there is a problem with invalid credentials. So I added validation WebAPI on IdS which is called before redirect on server to validate credentials (Client Credential Flow ) - with this solution user is redirect only with valid credentials and never see IdS server.

Question is does anybody see any security issue?

tejashdesai commented 9 years ago

HI pbartos,

Can you please let me know how you have passed acr_values to STS and called AuthenticateLocalAsync metthod to validate the user? And also how you manage the login errors i.e. invalida credentials. I have a same requirment.

Thanks, Tejash

leastprivilege commented 9 years ago

Looks like that this is what needs to be done (if "the business" demands it).

pbartos commented 9 years ago

leastprivilege: Ok, I'll take it like you don't see any big security issue in this approach, thx :)

tejashdesai:

At first, you need to add username and password into the auth challenge in client controller:

public ActionResult AutoLogin(string returnUrl)
    {
        var authProp = new AuthenticationProperties { RedirectUri = returnUrl };
        authProp.Dictionary.Add("acr_values", "username:jack password:pass");
        Request.GetOwinContext().Authentication.Challenge(authProp, "yourAuthType");
        return new HttpUnauthorizedResult();
    }

At the second, you need to manually put acr_values from challenge into the ProtocolMessage. You can do that with update OpenIdConnectAuthenticationOptions on the client Startup. I've created new OpenIdConnectAuthenticationOptions (NewOpenIdConnectAuthenticationOptions) to better readability (Don't forget to use this new option class in Startup config):

public class NewOpenIdConnectAuthenticationOptions : OpenIdConnectAuthenticationOptions
{
    public OriIdentityConnectAuthenticationOptions()
    {
        base.Notifications = new OpenIdConnectAuthenticationNotifications
        {
            RedirectToIdentityProvider = context => AddAdditionalAuthenticationParameters(context)
        };
    }

    public new OpenIdConnectAuthenticationNotifications Notifications
    {
        get { return base.Notifications; }
        set
        {
            throw new NotSupportedException("If you replace Notificationts instace you will lose passing acr_values (username, password, etc) to the server.");
        }
    }

    /// <summary>
    /// Add additional parameters to the authentication request.
    /// Used for passing username, password, etc.
    /// </summary>
    /// <param name="context"></param>
    /// <see cref="http://katanaproject.codeplex.com/workitem/325"/>
    /// <returns></returns>
    private static Task AddAdditionalAuthenticationParameters(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
    {
        if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest
            && context.OwinContext.Authentication.AuthenticationResponseChallenge != null)
        {
            var authenticationPropertis = context.OwinContext.Authentication.AuthenticationResponseChallenge.Properties.Dictionary;
            if (authenticationPropertis != null)
            {
                foreach (var pair in authenticationPropertis)
                {
                    context.ProtocolMessage.Parameters[pair.Key] = pair.Value;
                }
            }
        }

        return Task.FromResult(0);
    }
}

Now your credentials should be sent to the identityserver if you are using ImplicitFlow. For your automatic login on server you just need to override PreAuthenticateAsync method from UserService:

public override Task PreAuthenticateAsync(PreAuthenticationContext context)
    {
        return HandleLocalAuthenticationAsync(context);
    }

    private async Task HandleLocalAuthenticationAsync(PreAuthenticationContext context)
    {
        var userName = context.SignInMessage.AcrValues.Single(x => x.StartsWith("username:")).Remove(0, "username:".Length);
        var password = context.SignInMessage.AcrValues.Single(x => x.StartsWith("password:")).Remove(0, "password:".Length);
        context.AuthenticateResult = await AuthenticateLocalAsync(userName, password, context.SignInMessage);
    }

    private async Task<AuthenticateResult> AuthenticateLocalAsync(string username, string password, SignInMessage message)
    {
        var context = new LocalAuthenticationContext
        {
            UserName = username,
            Password = password,
            SignInMessage = message
        };

        await AuthenticateLocalAsync(context);
        return context.AuthenticateResult;
    }

Notes: 1) I've already integrated my solution into the company code so I'm not able to provide clean solution without lot of useless code around :/ But this should be enough for you :) 2) You should also implement some cryptographic function to encrypt and decrypt username and password in url. 3) Validation errors will be handle over my WebAPI on IdentityServer. MVC client just send Username & password and IdS WebAPI return if it's ok to use it to login. If not, there will be MVC validation error. If yes, then call MVC AutoLogin. 4) And how I'll handle IdentityServer errors is described here: https://github.com/IdentityServer/IdentityServer3/issues/1601

leastprivilege commented 9 years ago

yes - please don't send those credentials around in clear text...

tejashdesai commented 9 years ago

Thanks pbartos. You saved my days. I will try this approach.

tejashdesai commented 9 years ago

Hi pbartos, I tried your solution. Once AuthenticateLocalAsync method is called user redirect to Identity Login page. Is there any option to disable it? and what will be the startup class for both client and Idsvr? And how you implemented WebAPI to check user identity?

pbartos commented 9 years ago

Sorry for that. I've already update last code example. If there will be still problem, check what result you have in context.AuthenticateResult after AuthenticateLocalAsync. If context.AuthenticateResult is null then IdentityServer show you Login page. This is also happen when you send invalid credentials. That is a reason why I use Validation WebAPI.

There are almost no special startup configuration (don't forget NewOpenIdConnectAuthenticationOptions which I described previously). Just use Server/Client startup from documentation: https://identityserver.github.io/Documentation/docsv2/overview/mvcGettingStarted.html

For WebAPI you need to register new OWIN layer on IdentityServer. The problem is that you need to use new DI resolver because Autofac(DI resolver which is use on IdS Auth layer) is not accessible. So I use Windsor Castle. Rest is a pure WebAPI and there is a lot of examples on internet.

IdS Startup:

public void Configuration(IAppBuilder app)
    {
        XmlConfigurator.Configure();

        ConfigureAuth(app);
        ConfigureWebApi(app);
    }

public partial class Startup
{
    public void ConfigureWebApi(IAppBuilder app)
    {
        app.Map("/api", webApiApp =>
        {
            webApiApp.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
            {
                Authority = "https://localhost:44307/identity",
                RequiredScopes = new[] { "api" }
            });
            var config = new HttpConfiguration();

            //config.SuppressDefaultHostAuthentication();
            config.MapHttpAttributeRoutes();
            config.Filters.Add(new AuthorizeAttribute());

            var container = new WindsorContainer()
                .Install(
                new ControllerInstaller(),
                new ServiceInstaller()
                );

            config.DependencyResolver = new WindsorHttpDependencyResolver(container);

            webApiApp.UseWebApi(config);
        });
    }
}
pashute commented 8 years ago

Would this be the right way to go if business wants to control the claims (roles) that a calling client has through their regular user management service?

We would be using client credentials flow, but still adding an encrypted username/pw, authenticated in the user store by IDSrv3.

Sometimes the calling client may be external to the company. We could then notify them of pw / username changes, without changing anything in our IDSrv3 code.