Bitfiddler
Bitfiddler

Reputation: 4202

Aurelia-Authentication using Self Hosted Servicestack

Perhaps I'm using the wrong search terms but I can't find any information about how to get Aurelia-Authentication to play nice with ServiceStack. I am very unfamiliar with the super complicated authentication schemes used by websites so if I'm trying something that makes no sense it's probably because I'm confused. What I am trying to do is allow my users to log in using their windows credentials but to not have my web app require IIS for deployment (self hosted). So I need to transmit a username/password and have servicestack return something usable by Aurelia to store the authenticated session info. Right now I'm leaning towards using JWT.

Here is what I have on the client side (Aurelia):

main.ts

import { Aurelia } from 'aurelia-framework';
import 'src/helpers/exceptionHelpers'
import config from "./auth-config";

export function configure(aurelia: Aurelia) {
    aurelia.use
        .standardConfiguration()
        .feature('src/resources')
        .developmentLogging()
        .plugin('aurelia-dialog')
        .plugin('aurelia-api', config => {
            // Register an authentication hosts
            config.registerEndpoint('auth', 'http://localhost:7987/auth/');
        })
        .plugin('aurelia-authentication', (baseConfig) => {
            baseConfig.configure(config);
        });

    aurelia.start().then(x => x.setRoot('src/app'));
}

auth-config.ts

var config = {
    endpoint: 'auth',             // use 'auth' endpoint for the auth server
    configureEndpoints: ['auth'], // add Authorization header to 'auth' endpoint

    // The API specifies that new users register at the POST /users enpoint
    signupUrl: null,
    // The API endpoint used in profile requests (inc. `find/get` and `update`)
    profileUrl: null,
    // Logins happen at the POST /sessions/create endpoint
    loginUrl: '',
    // The API serves its tokens with a key of id_token which differs from
    // aurelia-auth's standard
    accessTokenName: 'BearerToken',
    // Once logged in, we want to redirect the user to the welcome view
    loginRedirect: '#/pending',
    // The SPA url to which the user is redirected after a successful logout
    logoutRedirect: '#/login',
    // The SPA route used when an unauthenticated user tries to access an SPA page that requires authentication
    loginRoute : '#/help'
};

export default config;

login.ts

import { AuthService } from 'aurelia-authentication';
import { inject, computedFrom } from 'aurelia-framework';

@inject(AuthService)
export class Login {
    heading: string;
    auth: AuthService;
    userName: string;
    password: string;

    constructor(authService) {
        this.auth = authService;
        this.heading = 'Login';
    }

    login() {
        var credentials = {
            username: this.userName,
            password: this.password,
            grant_type: "password"
        };
        return this.auth.login(credentials,
                               { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
        ).then(response => {
                console.log("success logged " + response);
            })
            .catch(err => {
                console.log("login failure");
            });
    }; 
}

Configuration on AppHost (serviceStack):

    public override void Configure(Container container)
    {
        var privateKey = RsaUtils.CreatePrivateKeyParams(RsaKeyLengths.Bit2048);
        var publicKey = privateKey.ToPublicRsaParameters();
        var privateKeyXml = privateKey.ToPrivateKeyXml();
        var publicKeyXml = privateKey.ToPublicKeyXml();

        SetConfig(new HostConfig
        {
#if DEBUG
            DebugMode = true,
            WebHostPhysicalPath = Path.GetFullPath(Path.Combine("~".MapServerPath(), "..", "..")),
#endif
        });
        container.RegisterAs<LDAPAuthProvider, IAuthProvider>();
        container.Register<ICacheClient>(new MemoryCacheClient { FlushOnDispose = false });
        container.RegisterAs<MemoryCacheClient, ICacheClient>();
        Plugins.Add(new AuthFeature(() => new AuthUserSession(),
            new[] {
                container.Resolve<IAuthProvider>(),
                new JwtAuthProvider {
                        HashAlgorithm = "RS256",
                        PrivateKeyXml = privateKeyXml,
                        RequireSecureConnection = false,
                    }
            })
        {
            HtmlRedirect = "~/#/pending",
            IncludeRegistrationService = false,
            IncludeAssignRoleServices = false,
            MaxLoginAttempts = Settings.Default.MaxLoginAttempts
        });
    }

I have the Authenticate attribute on the ServiceInterface I want to restrict access to.

Finally the LDAP provider:

public class LDAPAuthProvider : CredentialsAuthProvider
{
    private readonly IHoldingsManagerSettings _settings;

    public LDAPAuthProvider(IHoldingsManagerSettings settings)
    {
        _settings = settings;
    }
    public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
    {
        //Check to see if the username/password combo is valid, an exception will be thrown if the username or password is wrong
        try
        {
            var entry = new DirectoryEntry($"LDAP://{_settings.Domain}", userName, password);
            var nativeObject = entry.NativeObject;
            using (var identity = new WindowsIdentity(userName))
            {
                var principal = new WindowsPrincipal(identity);
                return principal.IsInRole(_settings.AdminGroupName);
            }
        }
        catch (Exception)
        {
            //This means the username/password combo failed
            return false;
        }
    }

    public override IHttpResult OnAuthenticated(IServiceBase authService,
                                                IAuthSession session,
                                                IAuthTokens tokens,
                                                Dictionary<string, string> authInfo)
    {
        //Fill IAuthSession with data you want to retrieve in the app eg:
        session.DisplayName = "Testy McTesterson";
        //...

        //Call base method to Save Session and fire Auth/Session callbacks:
        return base.OnAuthenticated(authService, session, tokens, authInfo);

        //Alternatively avoid built-in behavior and explicitly save session with
        //authService.SaveSession(session, SessionExpiry);
        //return null;
    }
}

So far, when I try to log in I manage to get as far as ServiceStack receiving the request in the LDAP provider, the authentication succeeds, but when the request comes back aurelia-authentication doesn't like the format of whatever ServiceStack is returning in it's session info.

I am certainly way off on my understanding of what is going on here. If someone could point me in the right direction on how to proceed I would really appreciate it.

Edit 1

Changed the 'accessTokenName' to 'BearerToken', seems to at least get the payload set. But still getting a failed authentication on the client side. Also need to figure out how to get Aurelia-Authentication to store the session in a cookie.

Edit 2

After much debugging, it appears that everything is working properly, the problem is that after the login is successful, I get redirected to a page that makes a call that must be authenticated. However I am having problems passing the authenticated Jwt Token using the servicestack JsonServiceClient, see here: ServiceStack Javascript JsonServiceClient missing properties

Upvotes: 1

Views: 277

Answers (1)

Bitfiddler
Bitfiddler

Reputation: 4202

Turns out the above LDAPprovider won't work the way you expect when you deploy to production (reasons beyond the scope of this thread).

If you include a reference to: System.DirectoryServices.AccountManagement

and change the the following method:

public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
    //Check to see if the username/password combo is valid, an exception will be thrown if the username or password is wrong
    try
    {
        var entry = new DirectoryEntry($"LDAP://{_settings.Domain}", userName, password);
        var nativeObject = entry.NativeObject;

        var ctx = new PrincipalContext(ContextType.Domain, _settings.Domain);
        var user = UserPrincipal.FindByIdentity(ctx, userName);
        if (user == null)
        {
            return false;
        }

        var group = GroupPrincipal.FindByIdentity(ctx, _settings.AdminGroupName);
        if (group == null)
        {
            return false;
        }

        return user.IsMemberOf(group);
    }
    catch (Exception)
    {
        //This means the username/password combo failed
        return false;
    }
}

Everything should work as expected.

Upvotes: 1

Related Questions