Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Auth0/Okta support #656

Closed
mshima opened this issue Apr 29, 2022 · 22 comments · Fixed by #658 or #660
Closed

Fix Auth0/Okta support #656

mshima opened this issue Apr 29, 2022 · 22 comments · Fixed by #658 or #660
Labels
$$ bug-bounty $$ https://www.jhipster.tech/bug-bounties/ $300 https://www.jhipster.tech/bug-bounties/

Comments

@mshima
Copy link
Member

mshima commented Apr 29, 2022

References:
#653 (comment)
#653 (comment)
#653 (comment)

Implementations:
generator-jhipster
svelte blueprint

@mraible
Copy link
Collaborator

mraible commented Apr 29, 2022

I tried changing oauth2.ts to use its own origin so a /callback handler doesn't need to be registered.

qs: {
  redirect_uri : window.location.origin,
  client_id: 'Tz7nrQ36DkcKmtuDMTZfi76tVMn7LDge',
  response_type: 'code',
  scope,
  audience,
},

I had to add http://localhost:8100 as an allowed callback URL and trusted origin in Auth0. This gets me a bit further, but it fails to get the location from the headers. I tried printing them and there is no location header.

Screen Shot 2022-04-29 at 12 13 00

@mshima
Copy link
Member Author

mshima commented Apr 29, 2022

@mraible I am wondering if we should create a package cypress-oauth2 to reuse some operations.

@mraible
Copy link
Collaborator

mraible commented Apr 29, 2022

That's not a bad idea. We could also read the .auth0.env from the backend app and make requests with the client secret like this example shows. https://auth0.com/blog/amp/end-to-end-testing-with-cypress-and-auth0/

@mshima
Copy link
Member Author

mshima commented Apr 29, 2022

I had to add http://localhost:8100 as an allowed callback URL and trusted origin in Auth0. This gets me a bit further, but it fails to get the location from the headers. I tried printing them and there is no location header.

From your screenshot:

Method: POST
URL:https://dev-06bzs1cu.us.autho.com/u/login
Headers:{
"Connection":"keep-alive"
"user-agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS x 10_15_7) ApplewebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36".
"accept":
"/"
"cookie":"XSRF-TOKEN=39a9623a-b380-4362-b837-fed3ef82a432"
"accept-encoding": "gzip, deflate"
"referer":"https://dev-06bzs1cu.us.autho.com/authorize/resume?state=Qy--WnmEtgaWwTnynhLCac129SRXGwgU"
}
Redirects:[
"302:https://dev-06bzs1cu.us.autho.com/authorize/resume?state=Qy--WnmEtgawwTnynhLCac1295RXGWgU"
"302:http://localhost:4200/callback?code=qMIFAgIuTwUx9rOorvRt2iTEdluKtxF5NaYPTDX0ur6g2"
}

The code should be extracted from the redirects.

@mraible
Copy link
Collaborator

mraible commented Apr 29, 2022

That worked!

diff --git a/ionic-app/cypress/integration/login.e2e-spec.ts b/ionic-app/cypress/integration/login.e2e-spec.ts
index 5ffe035..dd5ab50 100644
--- a/ionic-app/cypress/integration/login.e2e-spec.ts
+++ b/ionic-app/cypress/integration/login.e2e-spec.ts
@@ -18,7 +18,7 @@ describe('Login', () => {
     cy.login(ADMIN_USERNAME, ADMIN_PASSWORD);
     cy.visit('/');
 
-    const welcome = /Welcome, Admin/;
+    const welcome = /Welcome, Matt/;
     cy.get('app-home ion-title').invoke('text').should('match', welcome);
   });
 
diff --git a/ionic-app/cypress/support/oauth2.ts b/ionic-app/cypress/support/oauth2.ts
index 4bbf6ae..2a802c7 100644
--- a/ionic-app/cypress/support/oauth2.ts
+++ b/ionic-app/cypress/support/oauth2.ts
@@ -7,7 +7,6 @@ import { environment } from '../../src/environments/environment';
 const {
   oidcConfig: { redirect_url: redirect_uri, scopes: scope, audience },
 } = environment;
-
 // Get oauth2 basic data
 const getOauth2Data = () =>
   cy
@@ -26,7 +25,7 @@ const getOauth2Data = () =>
           info,
           configuration,
           qs: {
-            redirect_uri,
+            redirect_uri : window.location.origin,
             client_id: 'Tz7nrQ36DkcKmtuDMTZfi76tVMn7LDge',
             response_type: 'code',
             scope,
@@ -136,9 +135,8 @@ const oauthLogin = (username: string, password: string) => {
     } else {
       authorizeCode = keycloakLogin(oauth2Data, username, password);
     }
-    authorizeCode.then(({ headers }) => {
-      const { location } = headers;
-      const locationUrl = new URL(location as string);
+    authorizeCode.then(({ redirects }) => {
+      const locationUrl = new URL(redirects.pop().split(' ').pop());
       const code = locationUrl.searchParams.get('code');
 
       // Retrieve token.
@@ -151,8 +149,8 @@ const oauthLogin = (username: string, password: string) => {
           grant_type: 'authorization_code',
           code,
           refresh_token: undefined,
-          redirect_uri,
-          client_id: clientId,
+          redirect_uri: window.location.origin,
+          client_id: 'Tz7nrQ36DkcKmtuDMTZfi76tVMn7LDge',
         },
       }).then(({ body }) => {
         localStorage.setItem('CapacitorStorage.token_response', JSON.stringify(body));

I'll create a PR. We might want to make it so the client ID is always looked up from environment.ts and not the backend. That way, you only have to change it in one location.

@mraible
Copy link
Collaborator

mraible commented Apr 29, 2022

I've determined that Okta is quite a bit more difficult. Since the token endpoint requires PKCE, you have to send a code_challenge, code_challenge_method, and state parameter to the /authorize endpoint. Then, when you call the token endpoint, you have to send a code_verifier parameter.

You can see an overview of PKCE and how it works in https://developer.okta.com/blog/2019/08/22/okta-authjs-pkce.

A plain-JavaScript example can be found at https://github.com/aaronpk/pkce-vanilla-js/blob/master/index.html.

I'm going to add a bug bounty to this issue since it's more work than expected. It should be worth it though, PKCE should work with Keycloak, Auth0, and Okta. That's what Ionic AppAuth uses.

@mraible mraible changed the title Check Auth0/Okta support Fix Auth0/Okta support Apr 29, 2022
@mraible mraible added $$ bug-bounty $$ https://www.jhipster.tech/bug-bounties/ $300 https://www.jhipster.tech/bug-bounties/ labels Apr 29, 2022
@mraible
Copy link
Collaborator

mraible commented Apr 30, 2022

Reopening since Okta isn't working yet.

@mraible mraible reopened this Apr 30, 2022
@mshima
Copy link
Member Author

mshima commented Apr 30, 2022

@mraible you may want to test the login using cy.origin.
Uncomment, change change the origin first parameter with the comment above.

/* Login by ui, use it once cypress origin becomes stable enough.
cy.visit('/');
cy.get('#signIn').click();
cy.url({ timeout: 10000 }).should('includes', '/realms/');
cy.url().then(url => {
const { protocol, host } = new URL(url);
// eslint-disable-next-line @typescript-eslint/no-shadow
// `${protocol}//${host}/`
cy.origin('http://keycloak:9080', { args: { url, username, password } }, ({ url, username, password }) => {
// Reload oauth2 login page due to cypress origin change.
cy.visit(url);
cy.get('input[name="username"]').type(username);
cy.get('input[name="password"]').type(password);
cy.get('input[type="submit"]').click();
});
});
cy.url({ timeout: 10000 }).should('eq', Cypress.config().baseUrl + 'tabs/home');
*/

@mraible
Copy link
Collaborator

mraible commented Apr 30, 2022

@mshima I tried using this and it doesn't work. It gets to the Okta login form and fails to fill in the fields. I tried changing the field names to match the Okta form, but it doesn't help.

cy.get('input[name="identifier"]').type(username);
cy.get('input[name="credentials.passcode"]').type(password);

@mraible
Copy link
Collaborator

mraible commented Apr 30, 2022

I wonder if we could use AppAuth JS to do the OAuth dance for us? Here's an example that works in Electron.

https://github.com/oktadev/okta-appauth-js-electron-example/blob/master/flow.ts

@mshima
Copy link
Member Author

mshima commented Apr 30, 2022

I took a look at it first.
But I decided to ignore status/challenge and see if it worked.

IMO we should wait for some days if there are some movement to fix the cypress origin.
It should be better because it tests the application login for real.
We can have the login using cypress working and the application login broken.

@mraible
Copy link
Collaborator

mraible commented Apr 30, 2022

I tried the following, but it just hangs when waiting for okta to appear in the URL.

const login = (username: string, password: string) => {
  cy.session(
    [username, password],
    () => {
      cy.visit('/');
      cy.get('#signIn').click();
      cy.url({ timeout: 10000 }).should('includes', '/okta/');
      cy.url().then(url => {
        const { protocol, host } = new URL(url);
        // eslint-disable-next-line @typescript-eslint/no-shadow
        cy.origin(`${protocol}//${host}/`, { args: { url, username, password } }, ({ url, username, password }) => {
          // Reload oauth2 login page due to cypress origin change.
          cy.visit(url);
          cy.get('input[name="identifier"]').type(username);
          cy.get('input[name="credentials.passcode"]').type(password);
          cy.get('input[type="submit"]').click();
        });
      });
      cy.url({ timeout: 10000 }).should('eq', Cypress.config().baseUrl + 'tabs/home');
    },

    {
      validate: () => {
        cy.authenticatedRequest({ url: '/api/account' }).its('status').should('eq', 200);
      },
    }
  );
};

The URL doesn't change in the Cypress browser window.
Screen Shot 2022-04-30 at 10 37 28

@mshima
Copy link
Member Author

mshima commented Apr 30, 2022

@mshima
Copy link
Member Author

mshima commented Apr 30, 2022

By the way, I've created that project as a repro for cypress-io/cypress#21201 (comment).

@mraible
Copy link
Collaborator

mraible commented Apr 30, 2022

YAHOO - that worked!

const login = (username: string, password: string) => {
  cy.session(
    [username, password],
    () => {
      cy.visit('/');
      cy.get('#signIn').click();
      // cy.url({ timeout: 10000 }).should('includes', '/okta/');
      cy.url().then(url => {
        // eslint-disable-next-line @typescript-eslint/no-shadow
        cy.origin(`https://dev-17700857.okta.com/oauth2/default/v1/authorize`, { args: { url, username, password } }, ({ url, username, password }) => {
          cy.get('input[name="identifier"]').type(username);
          cy.get('input[name="credentials.passcode"]').type(password);
          cy.get('input[type="submit"]').click();
        });
      });
      cy.url({ timeout: 10000 }).should('eq', Cypress.config().baseUrl + 'tabs/home');
    },

    {
      validate: () => {
        cy.authenticatedRequest({ url: '/api/account' }).its('status').should('eq', 200);
      },
    }
  );
};

@mshima
Copy link
Member Author

mshima commented Apr 30, 2022

Great, the bug is with Keycloak them.
I was starting to setup my application for Okta.

@mshima
Copy link
Member Author

mshima commented Apr 30, 2022

So we need to get the url from auth-info and then try to use it at the cy.origin().

@mshima
Copy link
Member Author

mshima commented Apr 30, 2022

Something like:

import { getOauth2Data } from './oauth2';

const login = (username: string, password: string) => {
  cy.session(
    [username, password],
    () => {
      getOauth2Data().then(oauth2Data => {
        const {
          configuration: { authorization_endpoint },
        } = oauth2Data;
        const { protocol, host } = new URL(authorization_endpoint);
        cy.visit('/');
        cy.get('#signIn').click();
        // eslint-disable-next-line @typescript-eslint/no-shadow
        cy.origin(`${protocol}//${host}`, { args: { username, password } }, ({ username, password }) => {
          cy.get('input[name="identifier"]').type(username);
          cy.get('input[name="credentials.passcode"]').type(password);
          cy.get('input[type="submit"]').click();
        });
      )};
      cy.url({ timeout: 10000 }).should('eq', Cypress.config().baseUrl + 'tabs/home');
    },

    {
      validate: () => {
        cy.authenticatedRequest({ url: '/api/account' }).its('status').should('eq', 200);
      },
    }
  );
};

@mraible
Copy link
Collaborator

mraible commented Apr 30, 2022

This works for Keycloak, Okta, and Auth0!

/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-namespace */
import { apiHost } from './config';

const authenticatedRequest = (data: any) => {
  // eslint-disable-next-line @typescript-eslint/naming-convention
  const tokenResponse = localStorage.getItem('CapacitorStorage.token_response');
  if (!tokenResponse) {
    return Cypress.Promise.reject('token_response is missing');
  }
  const {access_token, token_type} = JSON.parse(tokenResponse);
  return cy.request({
    ...data,
    url: apiHost + data.url,
    headers: {
      ...data.headers,
      // eslint-disable-next-line @typescript-eslint/naming-convention
      Authorization: token_type !== 'Bearer' ? access_token : `Bearer ${access_token}`,
    },
  });
};

const getOauth2Data = () =>
  cy.request({
    url: `${apiHost}/api/auth-info`,
    followRedirect: false,
  }).then(({body: info}) => {
    const {issuer} = info;
    return cy
      .request({
        url: `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`,
        followRedirect: false,
      })
      .then(({body: configuration}) => ({
        configuration
      }));
  });

const login = (username: string, password: string) => {
  getOauth2Data().then(oauth2Data => {
    const {configuration: {authorization_endpoint}} = oauth2Data;
    cy.session(
      [username, password],
      () => {
        cy.visit('/');
        cy.get('#signIn').click();
        cy.origin(authorization_endpoint, {
          args: {authorization_endpoint, username, password}
          // eslint-disable-next-line @typescript-eslint/no-shadow
        }, ({authorization_endpoint, username, password}) => {
          let usernameElement = 'username';
          let passwordElement = 'password';
          if (authorization_endpoint.includes('okta')) {
            usernameElement = 'identifier';
            passwordElement = 'credentials.passcode';
          }
          cy.get(`input[name="${usernameElement}"]`).type(username);
          cy.get(`input[name="${passwordElement}"]`).type(password);
          cy.get('[type="submit"]').first().click();
        });
        cy.url({timeout: 10000}).should('eq', Cypress.config().baseUrl + 'tabs/home');
      },

      {
        validate: () => {
          cy.authenticatedRequest({url: '/api/account'}).its('status').should('eq', 200);
        },
      }
    );
  });
};

Cypress.Commands.addAll({
  authenticatedRequest,
  login,
});

declare namespace Cypress {
  interface Chainable<Subject = any> {
    login(username: string, password: string): typeof login;

    authenticatedRequest<T = any>(options: Partial<RequestOptions>): typeof authenticatedRequest;
  }
}

@mraible
Copy link
Collaborator

mraible commented Apr 30, 2022

PR to incorporate these changes: #660

@mshima
Copy link
Member Author

mshima commented May 3, 2022

The Keycloak issue was confirmed on cypress side.
cy.origin() only works on https or localhost for now.
It's easy to run Keycloak on https?

Should be followed at cypress-io/cypress#20685.

@mraible
Copy link
Collaborator

mraible commented May 4, 2022

We could recommend ngrok or other solutions for https with Keycloak.

https://developer.okta.com/blog/2022/01/31/local-https-java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
$$ bug-bounty $$ https://www.jhipster.tech/bug-bounties/ $300 https://www.jhipster.tech/bug-bounties/
Projects
None yet
2 participants