Skip to content

Commit

Permalink
Merge pull request #682 from pokt-foundation/stage
Browse files Browse the repository at this point in the history
feat: update UI with changes to support GCP Marketplace Integration signup flow
  • Loading branch information
fredteumer authored Oct 30, 2024
2 parents 045567b + 976a569 commit 99d42be
Show file tree
Hide file tree
Showing 5 changed files with 7,722 additions and 9,307 deletions.
4 changes: 2 additions & 2 deletions app/models/portal/mutations.graphqls
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mutation adminCreatePortalUser($email: String!, $providerUserID: ID!) {
adminCreatePortalUser(email: $email, providerUserID: $providerUserID) {
mutation adminCreatePortalUser ($email: String!, $providerUserID: ID!, $gcpAccountID: ID) {
adminCreatePortalUser (email: $email, providerUserID: $providerUserID, gcpAccountID: $gcpAccountID) {
portalUserID
email
iconURL
Expand Down
94 changes: 73 additions & 21 deletions app/routes/api.auth.auth0/route.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import jwt_decode from "jwt-decode"
import type { ActionFunction, LoaderFunction } from "@remix-run/node"
import { authenticator } from "~/utils/auth.server"

// The loader handles the case where the page is loaded via a GET request.
// This is where a standard GET browser request for login or signup is handled.
export let loader: LoaderFunction = ({ request }) => {

const url = new URL(request.url)
const signupField = url.searchParams.get("signup")
url.searchParams.append("prompt", "login")

if (signupField) {
if (url.searchParams.get("signup")) {
url.searchParams.append("screen_hint", "signup")
const signupRequest = new Request(url.toString(), request)
return authenticator.authenticate("auth0", signupRequest, {
Expand All @@ -22,33 +25,82 @@ export let loader: LoaderFunction = ({ request }) => {
})
}

// The action handles the case where the page is loaded via a POST request.
// This is where the POST redirect from a GCP Marketplace signup is handled.
export let action: ActionFunction = async ({ request }) => {
const formData = await request.formData()
const logoutField = formData.get("logout")
const signupField = formData.get("signup")

const url = new URL(request.url)
const formData = await request.formData();

if (logoutField) {
return authenticator.logout(request, {
redirectTo: url.origin,
})
// Possible scenarios:

// 1. GCP Marketplace signup - a POST redirect from the GCP Marketplace containing a JWT
const gcpMarketplaceToken = formData.get("x-gcp-marketplace-token") as string | null;
if (gcpMarketplaceToken) {
return handleGCPMarketplaceSignup(request, gcpMarketplaceToken);
}

if (signupField) {
url.searchParams.append("screen_hint", "signup")
url.searchParams.append("prompt", "login")
const signupRequest = new Request(url.toString(), request)
return authenticator.authenticate("auth0", signupRequest, {
successRedirect: "/account",
failureRedirect: "/",
})
// 2. Logout
if (formData.get("logout")) {
return handleLogout(request);
}

url.searchParams.append("prompt", "login")
const loginRequest = new Request(url.toString(), request)
// 3. Signup
if (formData.get("signup")) {
return handleSignup(request);
}

// 4. Login
return handleLogin(request);
}

// In the case where the page is loaded via a POST request from a GCP Marketplace signup,
// we need to decode the JWT containing the GCP account ID and pass it to Auth0 as a URL query param.
// Auth0 will then add it to a custom claim and pass it back to the callback page in the ID token.
async function handleGCPMarketplaceSignup(request: Request, gcpMarketplaceToken: string) {
const decodedToken = jwt_decode<{ sub: string }>(gcpMarketplaceToken);
const url = new URL(request.url);
url.searchParams.append("screen_hint", "signup");
url.searchParams.append("prompt", "login");

const gcpAccountID = decodedToken.sub;
if (gcpAccountID) {
url.searchParams.append("gcp_account_id", gcpAccountID);
}

const gcpSignupRequest = new Request(url.toString(), request);
return authenticator.authenticate("auth0", gcpSignupRequest, {
successRedirect: "/account",
failureRedirect: "/",
});
}

// handles the logout request from the client
async function handleLogout(request: Request) {
const url = new URL(request.url);
return authenticator.logout(request, {
redirectTo: url.origin,
});
}

// Handles a standard non-GCP Marketplace signup request from the client
async function handleSignup(request: Request) {
const url = new URL(request.url);
url.searchParams.append("screen_hint", "signup");
url.searchParams.append("prompt", "login");
const signupRequest = new Request(url.toString(), request);
return authenticator.authenticate("auth0", signupRequest, {
successRedirect: "/account",
failureRedirect: "/",
});
}

// Handles a standard login request from the client
async function handleLogin(request: Request) {
const url = new URL(request.url);
url.searchParams.append("prompt", "login");
const loginRequest = new Request(url.toString(), request);
return authenticator.authenticate("auth0", loginRequest, {
successRedirect: "/dashboard",
failureRedirect: url.origin,
})
});
}
108 changes: 80 additions & 28 deletions app/utils/auth.server.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Authenticator } from "remix-auth"
import { Auth0Strategy } from "remix-auth-auth0"
import invariant from "tiny-invariant"
import jwt_decode from "jwt-decode"
import { getRequiredServerEnvVar } from "./environment"
import { sessionStorage } from "./session.server"
import { initPortalClient } from "~/models/portal/portal.server"
import { User as PortalUser } from "~/models/portal/sdk"
import { User as PortalUser, AdminCreatePortalUserMutationVariables } from "~/models/portal/sdk"
import { initAdminPortal } from "~/utils/adminPortal"
import { getSdk as portalSDKType } from "~/models/portal/sdk"

// Create an instance of the authenticator, pass a generic with what your
// strategies will return and will be stored in the session
export const authenticator = new Authenticator<{
accessToken: string
refreshToken: string | undefined
// extraParams: Auth0ExtraParams
user: PortalUser & {
auth0ID: string
email_verified?: boolean
Expand All @@ -22,7 +23,6 @@ export const authenticator = new Authenticator<{
export type AuthUser = {
accessToken: string
refreshToken: string | undefined
// extraParams: Auth0ExtraParams
user: PortalUser & {
auth0ID: string
email_verified?: boolean
Expand All @@ -38,55 +38,107 @@ let auth0Strategy = new Auth0Strategy(
audience: getRequiredServerEnvVar("AUTH0_AUDIENCE"),
scope: getRequiredServerEnvVar("AUTH0_SCOPE"),
},
async ({ accessToken, refreshToken, extraParams, profile }): Promise<AuthUser> => {
async ({ accessToken, refreshToken, profile, extraParams }): Promise<AuthUser> => {

const email = profile?._json?.email
const providerUserID = profile?.id

invariant(email, "email is not found")
invariant(providerUserID, "providerUserID is not found")

let portalUser: AuthUser["user"]
const portalSDK = initPortalClient({ token: accessToken })

const portal = initPortalClient({ token: accessToken })
let portalUser: AuthUser["user"]

try {
// First try and get the portal user using the JWT
const getPortalUserResponse = await portal.getPortalUser()

// If portal user found, set it as the user
portalUser = {
...(getPortalUserResponse?.getPortalUser as PortalUser),
auth0ID: providerUserID,
email_verified: profile._json?.email_verified,
}

// Case 1: Standard login
portalUser = await handlePortalUserFound(portalSDK, providerUserID, profile)

} catch (error) {

const err = error as Error

// If portal user not found, create it or return the existing user
if (err.message.includes("Response not OK. 404 Not Found")) {
const portalAdmin = await initAdminPortal(portal)
const user = await portalAdmin.adminCreatePortalUser({
email,
providerUserID,
})

portalUser = {
...(user.adminCreatePortalUser as PortalUser),
auth0ID: providerUserID,
email_verified: profile._json?.email_verified,

const portalAdmin = await initAdminPortal(portalSDK)
const idToken = extraParams.id_token

// Decode the ID token to check for GCP account ID
const gcpAccountIDCustomClaim = "https://custom.claims/gcp_account_id"
const decodedIdToken = idToken ? jwt_decode<{ [key: string]: string | undefined }>(idToken) : undefined
const gcpAccountID = decodedIdToken?.[gcpAccountIDCustomClaim]

if (gcpAccountID) {

// Case 2: ID token contains gcp_account_id (GCP Marketplace signup)
portalUser = await handleGCPMarketplaceRedirect(portalAdmin, email, providerUserID, gcpAccountID)

} else {

// Case 3: ID token does not contain gcp_account_id (Standard signup)
portalUser = await handleStandardSignup(portalAdmin, email, providerUserID)
}
} else {
throw error
}

}

return {
accessToken,
refreshToken,
// extraParams,
user: portalUser,
}
},
)

authenticator.use(auth0Strategy)
// Handles the case where the portal user is found (standard login)
async function handlePortalUserFound(portalSDK: ReturnType<typeof portalSDKType>, providerUserID: string, profile: any): Promise<AuthUser["user"]> {

const getPortalUserResponse = await portalSDK.getPortalUser()

return {
...(getPortalUserResponse?.getPortalUser as PortalUser),
auth0ID: providerUserID,
email_verified: profile._json?.email_verified,
}
}

// Handles the case where the portal user is not found and id token contains gcp_account_id (GCP Marketplace signup)
async function handleGCPMarketplaceRedirect(portalAdmin: ReturnType<typeof portalSDKType>, email: string, providerUserID: string, gcpAccountID: string): Promise<AuthUser["user"]> {

const createGCPPortalUserVars: AdminCreatePortalUserMutationVariables = {
email,
providerUserID,
gcpAccountID,
}

const user = await portalAdmin.adminCreatePortalUser(createGCPPortalUserVars)

return {
...(user.adminCreatePortalUser as PortalUser),
auth0ID: providerUserID,
email_verified: true,
}
}

// Handles the case where the portal user is not found and no gcp_account_id is found (standard signup)
async function handleStandardSignup(portalAdmin: ReturnType<typeof portalSDKType>, email: string, providerUserID: string): Promise<AuthUser["user"]> {

const createPortalUserVars: AdminCreatePortalUserMutationVariables = {
email,
providerUserID,
}

const user = await portalAdmin.adminCreatePortalUser(createPortalUserVars)

return {
...(user.adminCreatePortalUser as PortalUser),
auth0ID: providerUserID,
email_verified: true,
}
}

// Use the Auth0 strategy for authentication
authenticator.use(auth0Strategy)
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/jest-axe": "^3.5.4",
"@types/jsonwebtoken": "^9.0.7",
"@types/mersenne-twister": "^1.1.7",
"@types/node": "^18.13.0",
"@types/react": "^18.0.25",
Expand Down Expand Up @@ -126,4 +127,4 @@
"engines": {
"node": "18.x"
}
}
}
Loading

0 comments on commit 99d42be

Please sign in to comment.