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

Auth: added support for common auth #94

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/auth/components/auth-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const AuthWidget: React.FC<{
</Button>
) : (
<LinkElement
def={{href: '/login', title: 'Login', variant: 'primary'} satisfies LinkDef}
def={{href: `${process.env.NEXT_PUBLIC_AUTH_ORIGIN}/login`, title: 'Login', variant: 'primary'} satisfies LinkDef}
className='h-8 w-fit !min-w-0'
/>
)
Expand Down
16 changes: 11 additions & 5 deletions packages/auth/components/login-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const LoginPanel: React.FC<PropsWithChildren & {
className?: string,
inputClx?: string,
noHeading?: boolean
onLoginChanged?: (loggedIn: boolean) => void
onLoginChanged?: (token: string) => void
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there's a significant issue here re api design. The previous way indicated whether someone was logging in our out. With your method, the token sort of indicates that. But what if the API fails? It might be better in the following ways:

  1. Let the client code and not the widget (sense a theme here??), handle what you do below. Originally this was just as intended as a way of creating alternative control flows. Let's keep it that way.

  2. if we do keep the api call here, we shoudl prob return the "in/out" boolean as well as the status of the api. So the method could take both.

I would like to review this with you and come up w something potentially cleaner, post MVP. Please create an issue for this w the launch tag

termsOfServiceUrl?: string
privacyPolicyUrl?: string
}> = observer(({
Expand All @@ -61,11 +61,17 @@ const LoginPanel: React.FC<PropsWithChildren & {

const [isLoading, setIsLoading] = useState(false)

const succeed = (loggedOut = false) => {
// If a callback is provide, don't redirect.
// Assume host code is handling (eg, mobile menu)
const succeed = async () => {
// If a callback is provide, don't redirect.
// Assume host code is handling (eg, mobile menu)
if (onLoginChanged) {
onLoginChanged(!loggedOut)
const res = await fetch(
'/api/auth/generate-custom-token',
{ method: 'POST' }
).then(res => res.json())
if (res.success) {
onLoginChanged(res.token.token)
}
}
else if (redirectUrl) {
router.push(redirectUrl)
Expand Down
19 changes: 19 additions & 0 deletions packages/auth/server/firebase-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ async function getSession() {
}
}

export const generateCustomToken = async (): Promise<{success: boolean, token: string | null}> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid the term "custom" as much as possible. It expresses very little. If you find yourself tempted to use it, try to thing of the added functionality above the "non-custom" version that you're adding, and then name it accordingly. This is minor but I'd like to see it changed. Perhaps along a another ticket. Up to you how to track.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

luxfi/web#199

"custom" was used because firebase calls it a custom token everywhere in the docs and their sign in function is called signInWithCustomToken()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct term.

const session = await getSession()

if (!(await isUserAuthenticated(session))) {
return {success: false, token: null}
}

const decodedIdToken = await auth.verifySessionCookie(session!)
const currentUser = await auth.getUser(decodedIdToken.uid)

try {
const token = await auth.createCustomToken(currentUser.uid)
return {success: true, token}
} catch (e) {
console.error('Error generating custom token', e)
return {success: false, token: null}
}
}

export async function createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions) {
return auth.createSessionCookie(idToken, sessionCookieOptions)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { getUserServerSide } from './firebase-support'
export { getUserServerSide, generateCustomToken } from './firebase-support'
export {
handleLogin as handleLoginApiRequest,
handleLogout as handleLogoutApiRequest
Expand Down
4 changes: 4 additions & 0 deletions packages/auth/service/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface AuthService {
provider: 'google' | 'facebook' | 'github'
) => Promise<{success: boolean, userInfo: HanzoUserInfo | null}>

loginWithCustomToken: (
token: string
) => Promise<{success: boolean, userInfo: HanzoUserInfo | null}>

associateWallet: () => Promise<void>

logout: () => Promise<{success: boolean}>
Expand Down
32 changes: 32 additions & 0 deletions packages/auth/service/impl/firebase-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
createUserWithEmailAndPassword,
type User,
signInWithEmailAndPassword,
signInWithCustomToken,
} from 'firebase/auth'

import { initializeApp, getApps } from "firebase/app"
Expand Down Expand Up @@ -140,6 +141,37 @@ export async function loginWithEmailAndPassword(
}
}

export async function loginWithCustomToken(
token: string,
): Promise<{success: boolean, user?: User }> {

let user: User | undefined = undefined
const userCredential = await signInWithCustomToken(auth, token)
user = userCredential.user

try {
const idToken = await user.getIdToken()

const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({ idToken }),
})
const resBody = (await response.json()) as unknown as APIResponse<string>

if (response.ok && resBody.success) {
return { success: true, user }
}
else {
return {success: false}
}
}
catch (error) {
console.error('Error signing in with Firebase auth', error)
return {success: false}
}
}

export async function logoutBackend(): Promise<{success: boolean}> {

try {
Expand Down
32 changes: 32 additions & 0 deletions packages/auth/service/impl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AuthServiceConf, HanzoUserInfo, HanzoUserInfoValue } from '../../t

import {
auth as fbAuth,
loginWithCustomToken,
loginWithEmailAndPassword,
loginWithProvider,
logoutBackend
Expand Down Expand Up @@ -133,6 +134,37 @@ class AuthServiceImpl implements AuthService {
}
}

loginWithCustomToken = async (
token: string
): Promise<{success: boolean, userInfo: HanzoUserInfo | null}> => {

try {
this._hzUser.clear()
const res = await loginWithCustomToken(token)
if (res.success && res.user) {
const walletAddress = res.user.email ? await getAssociatedWalletAddress(res.user.email) : undefined
this._hzUser.set({
email: res.user.email ?? '',
displayName : res.user.displayName ?? null,
walletAddress : walletAddress?.result ?? null
})

return {
success: true,
userInfo: this._hzUser
}
}
return {
success: false,
userInfo: null
}
}
catch (e) {
console.error('Error signing in with Firebase auth', e)
return {success: false, userInfo: null}
}
}

associateWallet = async (): Promise<void> => {
if (this._hzUser.isValid) {
const res = await associateWalletAddressWithAccount(this._hzUser.email)
Expand Down
Loading