Skip to main content

2 posts tagged with "auth0"

View All Tags

Cypress and Auth0

Cypress is a fantastic way to write UI tests for your web apps. Just world class. Wait, no. Galaxy class. I'm going to go one further: universe class. You get my drift.

Here's a pickle for you. You have functionality that lies only behind the walled garden of authentication. You want to write tests for these capabilities. Assuming that authentication takes place within your application that's no great shakes. Authentication is part of your app; it's no big deal using Cypress to automate logging in.

Auth is a serious business and, as Cypress is best in class for UI testing, I'll say that Auth0 is romping home with the same title in the auth-as-a-service space. My app is using Auth0 for authentication. What's important to note about this is the flow. Typically when using auth-as-a-service, the user is redirected to the auth provider's site to authenticate and then be redirected back to the application post-login.

Brian Mann (of Cypress fame) has been fairly clear when talking about testing with this sort of authentication flow:

You're trying to test SSO - and we have recipes showing you exactly how to do this.

Also best practice is never to visit or test 3rd party sites not under your control. You don't control microsoftonline, so there's no reason to use the UI to test this. You can programmatically test the integration between it and your app with cy.request - which is far faster, more reliable, and still gives you 100% confidence.

I want to automate logging into Auth0 from my Cypress tests. But hopefully in a good way. Not a bad way. Wouldn't want to make Brian sad.

Commanding Auth0#

To automate our login, we're going to use the auth0-js client library. This is the same library the application uses; but we're going to do something subtly different with it.

The application uses authorize to log users in. This function redirects the user into the Auth0 lock screen, and then, post authentication, redirects the user back to the application with a token in the URL. The app parses the token (using the auth0 client library) and sets the token and the expiration of said token in the browser sessionStorage.

What we're going to do is automate our login by using login instead. First of all, we need to add auth0-js as a dependency of our e2e tests:

yarn add auth0-js --dev

Next, we're going to create ourselves a custom command called loginAsAdmin:

const auth0 = require('auth0-js');
Cypress.Commands.add('loginAsAdmin', (overrides = {}) => {
Cypress.log({
name: 'loginAsAdminBySingleSignOn'
});
const webAuth = new auth0.WebAuth({
domain: 'my-super-duper-domain.eu.auth0.com', // Get this from https://manage.auth0.com/#/applications and your application
clientID: 'myclientid', // Get this from https://manage.auth0.com/#/applications and your application
responseType: 'token id_token'
});
webAuth.client.login(
{
realm: 'Username-Password-Authentication',
username: '[email protected]',
password: 'SoVeryVeryVery$ecure',
audience: 'myaudience', // Get this from https://manage.auth0.com/#/apis and your api, use the identifier property
scope: 'openid email profile'
},
function(err, authResult) {
// Auth tokens in the result or an error
if (authResult && authResult.accessToken && authResult.idToken) {
const token = {
accessToken: authResult.accessToken,
idToken: authResult.idToken,
// Set the time that the access token will expire at
expiresAt: authResult.expiresIn * 1000 + new Date().getTime()
};
window.sessionStorage.setItem('my-super-duper-app:storage_token', JSON.stringify(token));
} else {
console.error('Problem logging into Auth0', err);
throw err;
}
}
);
});

This command logs in using the auth0-js API and then sets the result into sessionStorage in the same way that our app does. This allows our app to read the value out of sessionStorage and use it. We're also going to put together one other command:

Cypress.Commands.add('visitHome', (overrides = {}) => {
cy.visit('/', {
onBeforeLoad: win => {
win.sessionStorage.clear();
}
})
});

This visits the root of our application and wipes the sessionStorage. This is necessary because Cypress doesn't clear down sessionStorage between tests. (That's going to change though.)

Using It#

Let's write a test that uses our new commands to see if it gets access to our admin functionality:

describe('access secret admin functionality', () => {
it('should be able to navigate to', () => {
cy.visitHome()
.loginAsAdmin()
.get('[href="/secret-adminny-stuff"]') // This link should only be visible to admins
.click()
.url()
.should('contain', 'secret-adminny-stuff/'); // non-admins should be redirected away from this url
});
});

Well, the test looks good but it's failing. If I fire up the Chrome Dev Tools in Cypress (did I mention that Cypress is absolutely fabulous?) then I see this response tucked away in the network tab:

{error: "unauthorized_client",} error : "unauthorized_client" error_description : "Grant type 'http://auth0.com/oauth/grant-type/password-realm' not allowed for the client."

Hmmm... So sad. If you go to https://manage.auth0.com/#/applications, select your application, Show Advanced Settings and Grant Types you'll see a Password option is unselected.

Select it, Save Changes and try again.

You now have a test which automates your Auth0 login using Cypress and goes on to test your application functionality with it!

One More Thing...#

It's worth saying that it's worth setting up different tenants in Auth0 to support your testing scenarios. This is generally a good idea so you can separate your testing accounts from Production accounts. Further to that, you don't need to have your Production setup supporting the Password``Grant Type.

Also, if you're curious about what the application under test is like then read this.

Auth0, TypeScript and ASP.NET Core

Most applications I write have some need for authentication and perhaps authorisation too. In fact, most apps most people write fall into that bracket. Here's the thing: Auth done well is a *big* chunk of work. And the minute you start thinking about that you almost invariably lose focus on the thing you actually want to build and ship.

So this Christmas I decided it was time to take a look into offloading that particular problem onto someone else. I knew there were third parties who provided Auth-As-A-Service - time to give them a whirl. On the recommendation of a friend, I made Auth0 my first port of call. Lest you be expecting a full breakdown of the various players in this space, let me stop you now; I liked Auth0 so much I strayed no further. Auth0 kicks AAAS. (I'm so sorry)

What I wanted to build#

My criteria for "auth success" was this:

  • I want to build a SPA, specifically a React SPA. Ideally, I shouldn't need a back end of my own at all
  • I want to use TypeScript on my client.

But, for when I do implement a back end:

  • I want that to be able to use the client side's Auth tokens to allow access to Auth routes on my server.
  • ‎I want to able to identify the user, given the token, to provide targeted data
  • Oh, and I want to use .NET Core 2 for my server.

And in achieving all of the I want to add minimal code to my app. Not War and Peace. My code should remain focused on doing what it does.

Boil a Plate#

I ended up with unqualified ticks for all my criteria, but it took some work to find out. I will say that Auth0 do travel the extra mile in terms of getting you up and running. When you create a new Client in Auth0 you're given the option to download a quick start using the technology of your choice.

This was a massive plus for me. I took the quickstart provided and ran with it to get me to the point of meeting my own criteria. You can use this boilerplate for your own ends. Herewith, a walkthrough:

The Walkthrough#

Fork and clone the repo at this location: https://github.com/johnnyreilly/auth0-react-typescript-asp-net-core.

What have we got? 2 folders, ClientApp contains the React app, Web contains the ASP.NET Core app. Now we need to get setup with Auth0 and customise our config.

Setup Auth0#

Here's how to get the app set up with Auth0; you're going to need to sign up for a (free) Auth0 account. Then login into Auth0 and go to the management portal.

Client#

  • Create a Client with the name of your choice and use the Single Page Web Applications template.
  • From the new Client Settings page take the Domain and Client ID and update the similarly named properties in the appsettings.Development.json and appsettings.Production.json files with these settings.
  • To the Allowed Callback URLs setting add the URLs: http://localhost:3000/callback,http://localhost:5000/callback - the first of these faciliates running in Debug mode, the second in Production mode. If you were to deploy this you'd need to add other callback URLs in here too.

API#

  • Create an API with the name of your choice (I recommend the same as the Client to avoid confusion), an identifier which can be anything you like; I like to use the URL of my app but it's your call.
  • From the new API Settings page take the Identifier and update the Audience property in the appsettings.Development.json and appsettings.Production.json files with that value.

Running the App#

Production build#

Build the client app with yarn build in the ClientApp folder. (Don't forget to yarn install first.) Then, in the Web folder dotnet restore, dotnet run and open your browser to http://localhost:5000

Debugging#

Run the client app using webpack-dev-server using yarn start in the ClientApp folder. Fire up VS Code in the root of the repo and hit F5 to debug the server. Then open your browser to http://localhost:3000

The Tour#

When you fire up the app you're presented with "you are not logged in!" message and the option to login. Do it, it'll take you to the Auth0 "lock" screen where you can sign up / login. Once you do that you'll be asked to confirm access:

All this is powered by Auth0's auth0-js npm package. (Excellent type definition files are available from Definitely Typed; I'm using the @types/auth0-js package DT publishes.) Usage of which is super simple; it exposes an authorize method that when called triggers the Auth0 lock screen. Once you've "okayed" you'll be taken back to the app which will use the parseHash method to extract the access token that Auth0 has provided. Take a look at how our authStore makes use of auth0-js: (don't be scared; it uses mobx - but you could use anything)

authStore.ts#

import { Auth0UserProfile, WebAuth } from 'auth0-js';
import { action, computed, observable, runInAction } from 'mobx';
import { IAuth0Config } from '../../config';
import { StorageFacade } from '../storageFacade';
interface IStorageToken {
accessToken: string;
idToken: string;
expiresAt: number;
}
const STORAGE_TOKEN = 'storage_token';
export class AuthStore {
@observable.ref auth0: WebAuth;
@observable.ref userProfile: Auth0UserProfile;
@observable.ref token: IStorageToken;
constructor(config: IAuth0Config, private storage: StorageFacade) {
this.auth0 = new WebAuth({
domain: config.domain,
clientID: config.clientId,
redirectUri: config.redirectUri,
audience: config.audience,
responseType: 'token id_token',
scope: 'openid email profile do:admin:thing' // the do:admin:thing scope is custom and defined in the scopes section of our API in the Auth0 dashboard
});
}
initialise() {
const token = this.parseToken(this.storage.getItem(STORAGE_TOKEN));
if (token) {
this.setSession(token);
}
this.storage.addEventListener(this.onStorageChanged);
}
parseToken(tokenString: string) {
const token = JSON.parse(tokenString || '{}');
return token;
}
onStorageChanged = (event: StorageEvent) => {
if (event.key === STORAGE_TOKEN) {
this.setSession(this.parseToken(event.newValue));
}
}
@computed get isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
return this.token && new Date().getTime() < this.token.expiresAt;
}
login = () => {
this.auth0.authorize();
}
handleAuthentication = () => {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
const token = {
accessToken: authResult.accessToken,
idToken: authResult.idToken,
// Set the time that the access token will expire at
expiresAt: authResult.expiresIn * 1000 + new Date().getTime()
};
this.setSession(token);
} else if (err) {
// tslint:disable-next-line:no-console
console.log(err);
alert(`Error: ${err.error}. Check the console for further details.`);
}
});
}
@action
setSession(token: IStorageToken) {
this.token = token;
this.storage.setItem(STORAGE_TOKEN, JSON.stringify(token));
}
getAccessToken = () => {
const accessToken = this.token.accessToken;
if (!accessToken) {
throw new Error('No access token found');
}
return accessToken;
}
@action
loadProfile = async () => {
const accessToken = this.token.accessToken;
if (!accessToken) {
return;
}
this.auth0.client.userInfo(accessToken, (err, profile) => {
if (err) { throw err; }
if (profile) {
runInAction(() => this.userProfile = profile);
return profile;
}
return undefined;
});
}
@action
logout = () => {
// Clear access token and ID token from local storage
this.storage.removeItem(STORAGE_TOKEN);
this.token = null;
this.userProfile = null;
}
}

Once you're logged in the app offers you more in the way of navigation options. A "Profile" screen shows you the details your React app has retrieved from Auth0 about you. This is backed by the client.userInfo method on auth0-js. There's also a "Ping" screen which is where your React app talks to your ASP.NET Core server. The screenshot below illustrates the result of hitting the "Get Private Data" button:

The "Get Server to Retrieve Profile Data" button is interesting as it illustrates that the server can get access to your profile data as well. There's nothing insecure here; it gets the details using the access token retrieved from Auth0 by the ClientApp and passed to the server. It's the API we set up in Auth0 that is in play here. The app uses the Domain and the access token to talk to Auth0 like so:

UserController.cs#

// Retrieve the access_token claim which we saved in the OnTokenValidated event
var accessToken = User.Claims.FirstOrDefault(c => c.Type == "access_token").Value;
// If we have an access_token, then retrieve the user's information
if (!string.IsNullOrEmpty(accessToken))
{
var domain = _config["Auth0:Domain"];
var apiClient = new AuthenticationApiClient(domain);
var userInfo = await apiClient.GetUserInfoAsync(accessToken);
return Ok(userInfo);
}

We can also access the sub claim, which uniquely identifies the user:

UserController.cs#

// We're not doing anything with this, but hey! It's useful to know where the user id lives
var userId = User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier).Value; // our userId is the sub value

The reason our ASP.NET Core app works with Auth0 and that we have access to the access token here in the first place is because of our startup code:

Startup.cs#

public void ConfigureServices(IServiceCollection services)
{
var domain = $"https://{Configuration["Auth0:Domain"]}/";
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = domain;
options.Audience = Configuration["Auth0:Audience"];
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
if (context.SecurityToken is JwtSecurityToken token)
{
if (context.Principal.Identity is ClaimsIdentity identity)
{
identity.AddClaim(new Claim("access_token", token.RawData));
}
}
return Task.FromResult(0);
}
};
});
// ....

Authorization#

We're pretty much done now; just one magic button to investigate: "Get Admin Data". If you presently try and access the admin data you'll get a 403 Forbidden. It's forbidden because that endpoint relies on the "do:admin:thing" scope in our claims:

UserController.cs#

[Authorize(Scopes.DoAdminThing)]
[HttpGet("api/userDoAdminThing")]
public IActionResult GetUserDoAdminThing()
{
return Ok("Admin endpoint");
}

Scopes.cs#

public static class Scopes
{
// the do:admin:thing scope is custom and defined in the scopes section of our API in the Auth0 dashboard
public const string DoAdminThing = "do:admin:thing";
}

This wired up in our ASP.NET Core app like so:

Startup.cs#

services.AddAuthorization(options =>
{
options.AddPolicy(Scopes.DoAdminThing, policy => policy.Requirements.Add(new HasScopeRequirement(Scopes.DoAdminThing, domain)));
});
// register the scope authorization handler
services.AddSingleton<iauthorizationhandler, hasscopehandler="">();
</iauthorizationhandler,>

HasScopeHandler.cs#

public class HasScopeHandler : AuthorizationHandler<hasscoperequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
// If user does not have the scope claim, get out of here
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
return Task.CompletedTask;
// Split the scopes string into an array
var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' ');
// Succeed if the scope array contains the required scope
if (scopes.Any(s => s == requirement.Scope))
context.Succeed(requirement);
return Task.CompletedTask;
}
}
</hasscoperequirement>

The reason we're 403ing at present is because when our HasScopeHandler executes, requirement.Scope has the value of "do:admin:thing" and our scopes do not contain that value. To add it, go to your API in the Auth0 management console and add it:

Note that you can control how this scope is acquired using "Rules" in the Auth0 management portal.

You won't be able to access the admin endpoint yet because you're still rocking with the old access token; pre-newly-added scope. But when you next login to Auth0 you'll see a prompt like this:

Which demonstrates that you're being granted an extra scope. With your new shiny access token you can now access the oh-so-secret Admin endpoint.

I had some more questions about Auth0 as I'm still new to it myself. To see my question (and the very helpful answer!) go here: https://community.auth0.com/questions/13786/get-user-data-server-side-what-is-a-good-approach