Skip to content

Commit

Permalink
Replace react-google-login with odic-client
Browse files Browse the repository at this point in the history
The underlying library react-google-login uses is deprecated, and is being discontinued on 31st March 2023: https://developers.googleblog.com/2021/08/gsi-jsweb-deprecation.html

Picked oidc-client over:
- @react-oauth/google: Google and React specific, makes it harder to migrate to anything else. Appreciate this is what we had beforehand, but now we seem closer to changing identity provider want to keep that flexibility.
- google-oauth-gsi: Google speicifc, so similar to above. Also seems to largely be @react-oauth/google repacked without react bits - without potential licensing issues.
- oidc-client-ts: Would have preferred to use this, but it isn't currently compatible with the way Google does OAuth (see authts/oidc-client-ts#152)
  • Loading branch information
domdomegg committed Jan 19, 2023
1 parent 3362175 commit b50868a
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 73 deletions.
78 changes: 54 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions server/src/schemas/jsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ export const $GoogleLoginRequest: JSONSchema<S.GoogleLoginRequest> = {
type: "object",
properties: {
idToken: { type: "string" },
accessToken: { type: "string" },
},
required: ["idToken", "accessToken"],
required: ["idToken"],
additionalProperties: false,
}

Expand Down
1 change: 0 additions & 1 deletion server/src/schemas/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export interface LoginResponse {

export interface GoogleLoginRequest {
idToken: string;
accessToken: string;
}

export interface ImpersonationLoginRequest {
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"jsonexport": "^3.2.0",
"oidc-client": "^1.11.5",
"postcss": "^8.3.6",
"react": "^17",
"react-dom": "^17",
"react-google-login": "^5.2.2",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.22.5",
"react-timeago": "^7.1.0",
Expand Down
1 change: 0 additions & 1 deletion web/src/helpers/generated-api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export interface LoginResponse {

export interface GoogleLoginRequest {
idToken: string;
accessToken: string;
}

export interface ImpersonationLoginRequest {
Expand Down
4 changes: 3 additions & 1 deletion web/src/pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Page from "../../components/Page"
import FundraisersPage from "./fundraisers"
import ProfilePage from "./profile"
import TasksPage from "./tasks"
import Login from "./login"
import Login, { OauthCallbackPage } from "./login"
import NotFoundPage from "../404"
import Navigation from "../../components/Navigation"
import FundraiserPage from "./fundraiser"
Expand Down Expand Up @@ -82,6 +82,8 @@ const IndexLayout = () => {
</Section>
)}
<Router basepath="/admin" className="text-left">
<OauthCallbackPage path="/oauth-callback" />

{auth && (
<>
<FundraisersPage path="/" />
Expand Down
96 changes: 53 additions & 43 deletions web/src/pages/admin/login.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as React from "react"
import { RouteComponentProps } from "@gatsbyjs/reach-router"
import { useGoogleLogin, GoogleLoginResponse } from "react-google-login"
// We are using oidc-client rather than oidc-client-ts because it supports
// the implicit flow, which is currently needed for Google authentication
// https://github.com/authts/oidc-client-ts/issues/152
import { UserManager } from "oidc-client"
import Section, { SectionTitle } from "../../components/Section"
import Alert from "../../components/Alert"
import Logo from "../../components/Logo"
import { useAuthState, useRawAxios } from "../../helpers/networking"
import { useAuthState, useRawAxios, useRawReq } from "../../helpers/networking"
import env from "../../env/env"
import Button from "../../components/Button"
import { LoginResponse } from "../../helpers/generated-api-client"
Expand Down Expand Up @@ -54,58 +57,46 @@ const googleRequiredScopes = [
"https://www.googleapis.com/auth/userinfo.profile",
]

const userManager = new UserManager({
authority: "https://accounts.google.com",
client_id: env.GOOGLE_LOGIN_CLIENT_ID,
redirect_uri: "http://localhost:8000/admin/oauth-callback",
scope: googleRequiredScopes.join(" "),
response_type: "id_token",
})

const GoogleLoginForm: React.FC<LoginFormProps> = ({ setError, setLoading }) => {
const [_, setAuthState] = useAuthState()
const axios = useRawAxios()
const googleLogin = useGoogleLogin({
clientId: env.GOOGLE_LOGIN_CLIENT_ID,
scope: googleRequiredScopes.join(" "),
onScriptLoadFailure: () => {
setError("Failed to load Google login script")
},
onRequest: () => {
setError(undefined)
setLoading("Waiting on Google login...")
},
onSuccess: async (_res) => {
// We can remove this override once the TypeScript definitions are improved:
// https://github.com/anthonyjgrove/react-google-login/pull/482
const res = _res as GoogleLoginResponse

const grantedScopes = res.tokenObj.scope.split(" ")
const missingScopes = googleRequiredScopes.filter((s) => !grantedScopes.includes(s))
if (missingScopes.length > 0) {
setError(`Missing scopes: ${JSON.stringify(missingScopes)}`)
} else {
const req = useRawReq()

return (
<Button
onClick={async () => {
setLoading("Waiting on Google login...")
try {
const user = await userManager.signinPopup()
setLoading(true)
const loginResponse = await axios.post<LoginResponse>("/admin/login/google", { idToken: res.tokenId, accessToken: res.accessToken })

const missingScopes = googleRequiredScopes.filter((s) => !user.scopes.includes(s))
if (missingScopes.length > 0) {
throw new Error(`Missing scopes: ${JSON.stringify(missingScopes)}`)
}

const loginResponse = await req(
"post /admin/login/google",
{ idToken: user.id_token },
)

setAuthState({
token: loginResponse.data.accessToken,
expiresAt: loginResponse.data.expiresAt,
groups: loginResponse.data.groups,
})
} catch (err) {
// eslint-disable-next-line no-console
console.error(err)
setError(err instanceof Error ? err : String(err))
setLoading(false)
setError(err)
}
}
},
onFailure: (err) => {
// eslint-disable-next-line no-console
console.error(err)
const errorMessage = [err.message, err.error, err.details].filter((s) => s).join(": ")
setError(errorMessage.length > 0 ? errorMessage : String(err))
setLoading(false)
},
})

return (
<Button
onClick={googleLogin.signIn}
disabled={!googleLogin.loaded}
setLoading(false)
}}
>
Google Login
</Button>
Expand Down Expand Up @@ -149,4 +140,23 @@ const ImpersonationLoginForm: React.FC<LoginFormProps> = ({ setError, setLoading
)
}

export const OauthCallbackPage: React.FC<RouteComponentProps> = () => {
const [error, setError] = React.useState<undefined | React.ReactNode | Error>()

React.useEffect(() => {
try {
userManager.signinCallback()
} catch (err) {
setError(err)
}
}, [])

return (
<Section className="mt-8 text-center">
{error && <Alert variant="error">{error}</Alert>}
{!error && <h1>Logging you in...</h1>}
</Section>
)
}

export default Login

0 comments on commit b50868a

Please sign in to comment.