Skip to content

Commit

Permalink
Update Auth0 for v17
Browse files Browse the repository at this point in the history
  • Loading branch information
mraible committed Dec 3, 2023
1 parent 26b10ff commit 678a2dc
Show file tree
Hide file tree
Showing 18 changed files with 2,896 additions and 192 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ testem.log
# System files
.DS_Store
Thumbs.db

*.env.json
211 changes: 204 additions & 7 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1530,13 +1530,210 @@ A completed project with this code in it is available on GitHub at https://githu

I hope you've enjoyed this in-depth tutorial on how to get started with Angular and Angular CLI. Angular CLI takes much of the pain out of setting up an Angular project and using Typescript. I expect great things from Angular CLI, mostly because the Angular setup process can be tedious and CLI greatly simplifies things.

== Bonus: Angular Material, Bootstrap, Auth0, and Electron
== Bonus: Auth0

If you'd like to see how to integrate https://material.angular.io/[Angular Material], https://getbootstrap.com[Bootstrap], https://developer.auth0.com[Auth0], or https://www.electronjs.org/[Electron] this section is for you!
To integrate http://developer.auth0.com[Auth0] for user authentication, you'll first need to https://auth0.com/signup[register] and create an OpenID Connect application.

I've created branches to show how to integrate each of these libraries. Click on the links below to see each branch's documentation.
=== Create an OpenID Connect App in Auth0

* https://github.com/mraible/ng-demo/tree/angular-material#bonus-angular-material[Angular Material]
* https://github.com/mraible/ng-demo/tree/bootstrap#bonus-bootstrap[Bootstrap]
* https://github.com/mraible/ng-demo/tree/auth0#bonus-auth0[Auth0]
* https://github.com/mraible/ng-demo/tree/electron#bonus-electron[Electron]
OpenID Connect is built on top of the OAuth 2.0 protocol. It allows clients to verify the identity of the user and, as well as to obtain their basic user information.

Install the https://github.com/auth0/auth0-cli#installation[Auth0 CLI] and run `auth0 login` to register your account. Then, run `auth0 apps create`. Specify a name and description of your choosing. Choose **Single Page Web Application** and use `\http://localhost:4200/home` for the Callback URL. Specify `\http://localhost:4200` for the rest of the URLs.

=== Add OIDC Authentication with OktaDev Schematics

Use https://github.com/oktadev/schematics[OktaDev Schematics] to add OAuth 2.0 and OpenID Connect (OIDC) support.

----
ng add @oktadev/schematics --auth0
----

You'll be prompted for an issuer and client ID. You should have these from the OIDC app you just created.

This process will perform the following steps for you:

1. Install the https://github.com/auth0/auth0-angular[Auth0 Angular SDK].
2. Update `src/app/app.config.ts` with your OIDC configuration and initialization logic.
3. Configure an `AuthHttpInterceptor` that adds an `Authorization` header with an access token to outbound requests.
4. Create a `HomeComponent` and configure it with authentication logic.
5. Update unit tests for `AppComponent` and `HomeComponent` to mock Auth0.

Update `app.routes.ts` to add a route guard to the `/search` and `/edit` routes.

[source,typescript]
.src/app/app-routing.ts
----
import { AuthGuard } from '@auth0/auth0-angular';
export const routes: Routes = [
...
{ path: 'search', component: SearchComponent, canActivate: [AuthGuard] },
{ path: 'edit/:id', component: EditComponent, canActivate: [AuthGuard] }
];
----

You'll also need to update the `app.component.spec.ts` file's last test to look for the correct welcome message.

[source,typescript]
----
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Welcome to ng-demo!');
});
----

After making these changes, you should be able to run `ng serve` and see a login button at `http://localhost:4200/home`.

.Auth0 login button
image::src/assets/images/auth0-login-button.png[Login button, 800, scaledwidth="100%", align=center]

Click the *Login* button and sign in with one of the users that's configured in your Auth0 application or sign up as a new user.

.Auth0 login form
image::src/assets/images/auth0-login-form.png[Auth0 login form, 800, scaledwidth="100%", align=center]

==== Display Authenticated User's Name

To display the authenticated user's name, you can use the `user$` observable on the `AuthService` instance.

Modify `home.component.html` to display a welcome message to the user and provide them with a link to search.

[source,html]
.src/app/home/home.component.html
----
<div>
@if (auth.user$ | async; as user) {
<h2>Welcome, {{user?.name}}!</h2>
<p><a routerLink="/search" routerLinkActive="active">Search</a></p>
}
@if ((auth.isAuthenticated$ | async) === false) {
<button (click)="login()">Login</button>
} @else {
<button (click)="logout()">Logout</button>
}
</div>
----

Refresh your app, and you should see your name with a link to *Search*.

.View after login
image::src/assets/images/auth0-post-login.png[View after login, 800, scaledwidth="100%", align=center]

If you log out and manually navigate to `http://localhost:4200/search`, you'll be required to log in.

If everything works—congrats!

==== Add Authentication to Cypress tests

To make it so you can run your e2e tests with authentication, add a `signIn()` Cypress command in `cypress/support/commands.ts`.

[source,typescript]
----
Cypress.Commands.add('signIn', (username, password) => {
Cypress.log({
message: [`🔐 Authenticating: ${username}`],
autoEnd: false,
})
cy.origin(Cypress.env('E2E_DOMAIN'), {args: {username, password}},
({username, password}) => {
cy.get('input[name=username]').type(username);
cy.get('input[name=password]').type(password, {log: false});
cy.get('button[type=submit]').first().click();
}
)
cy.url().should('equal', 'http://localhost:4200/home')
})
----

Then, in `cypress/support/e2e.ts`, uncomment the import for `commands` and specify `before()` and `after()` functions that log in and log out before each test.

[source,typescript]
----
import './commands';
beforeEach(() => {
cy.visit('/')
cy.get('#login').click()
cy.signIn(
Cypress.env('E2E_USERNAME'),
Cypress.env('E2E_PASSWORD')
)
})
afterEach(() => {
cy.visit('/')
cy.get('#logout').click()
})
----

Modify `cypress/e2e/home.cy.ts` to remove the line with `cy.visit('/')`.

=== Don't Store Credentials in Source Control

Don't store your username and password in `cypress.config.ts`. This is convenient, but a bad practice.

You can solve it by using https://docs.cypress.io/guides/guides/environment-variables#Option-2-cypressenvjson[`cypress.env.json`].

Create a `cypress.env.json` file in your project's root folder with your Auth0 credentials in it.

[source,json]
----
{
"E2E_DOMAIN": "<your domain>",
"E2E_USERNAME": "<your username>",
"E2E_PASSWORD": "<your password>"
}
----

Add `*.env.json` to your `.gitignore` file to prevent this file from being checked in.

Now, `npm run cypress:run` should work the same as before.

=== Update GitHub Actions

If you're using GitHub Actions to test your project, you'll need to update the Cypress workflow to include your domain and credentials.

[source,yaml]
----
- name: Run integration tests
uses: cypress-io/github-action@v5
with:
browser: chrome
start: npm start
install: false
wait-on: http://[::1]:4200
env:
CYPRESS_E2E_DOMAIN: ${{ secrets.E2E_DOMAIN }}
CYPRESS_E2E_USERNAME: ${{ secrets.E2E_USERNAME }}
CYPRESS_E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
----

Then, create repository secrets on GitHub for `E2E_DOMAIN`, `E2E_USERNAME` and `E2E_PASSWORD`.

You also might find it useful to upload screenshots of your test failures to GitHub. Add the following to your workflow.

[source,yaml]
----
- name: Upload screenshots on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: cypress/screenshots
----

You can then download the screenshots by going to a job's summary. From there, scroll down to the *Artifacts* section and click on the *cypress-screenshots* artifact.

If you encounter any issues, please post a question to Stack Overflow with an http://stackoverflow.com/questions/tagged/auth0[auth0 tag], or hit me up on Twitter https://twitter.com/mraible[@mraible].

The code in this branch can be downloaded using the following command:

----
git clone -b auth0 https://github.com/mraible/ng-demo.git
----

https://github.com/mraible/ng-demo/compare/main\...auth0[Click here to see the difference] between this branch and the main branch.
5 changes: 5 additions & 0 deletions cypress.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"E2E_DOMAIN": "dev-06bzs1cu.us.auth0.com",
"E2E_USERNAME": "mraible",
"E2E_PASSWORD": "@qaG7x2qqdD8BhVxGmEU"
}
1 change: 0 additions & 1 deletion cypress/e2e/home.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
describe('Home', () => {
it('Visits the initial project page', () => {
cy.visit('/')
cy.contains('Welcome to ng-demo!')
cy.contains('Search')
})
Expand Down
59 changes: 16 additions & 43 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,16 @@
// ***********************************************
// This example namespace declaration will help
// with Intellisense and code completion in your
// IDE or Text Editor.
// ***********************************************
// declare namespace Cypress {
// interface Chainable<Subject = any> {
// customCommand(param: any): typeof customCommand;
// }
// }
//
// function customCommand(param: any): void {
// console.warn(param);
// }
//
// NOTE: You can use it like so:
// Cypress.Commands.add('customCommand', customCommand);
//
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('signIn', (username, password) => {
Cypress.log({
message: [`🔐 Authenticating: ${username}`],
autoEnd: false,
})

cy.origin(Cypress.env('E2E_DOMAIN'), {args: {username, password}},
({username, password}) => {
cy.get('input[name=username]').type(username);
cy.get('input[name=password]').type(password, {log: false});
cy.get('button[type=submit]').first().click();
}
)

cy.url().should('equal', 'http://localhost:4200/home')
})
30 changes: 14 additions & 16 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands';

// When a command from ./commands is ready to use, import with `import './commands'` syntax
// import './commands';
beforeEach(() => {
cy.visit('/')
cy.get('#login').click()
cy.signIn(
Cypress.env('E2E_USERNAME'),
Cypress.env('E2E_PASSWORD')
)
})

afterEach(() => {
cy.visit('/')
cy.get('#logout').click()
})
Loading

0 comments on commit 678a2dc

Please sign in to comment.