From 08f5271a32c6a68d4943490f7a27dfb5b7f0f0d5 Mon Sep 17 00:00:00 2001 From: Alder Whiteford Date: Tue, 18 Jun 2024 12:54:41 -0400 Subject: [PATCH] OAuth Impl --- backend/entities/auth/base/handlers.go | 2 +- backend/integrations/oauth/crypt/crypt.go | 2 +- .../integrations/oauth/soth/sothic/sothic.go | 5 +- backend/middleware/auth/user.go | 8 +- backend/server/server.go | 2 +- frontend/lib/package.json | 2 +- frontend/lib/src/api/authApi.ts | 58 ++++---------- frontend/lib/src/api/base.ts | 2 +- frontend/lib/src/api/userApi.ts | 11 --- frontend/lib/src/types/auth.ts | 24 ++---- frontend/lib/src/types/user.ts | 8 +- frontend/mobile/package.json | 2 +- frontend/mobile/src/app/_layout.tsx | 7 +- .../mobile/src/app/app/(tabs)/calendar.tsx | 3 +- .../mobile/src/app/app/(tabs)/profile.tsx | 24 +++--- .../app/event/components/upcoming-events.tsx | 2 +- frontend/mobile/src/app/auth/callback.tsx | 71 ++++++++++++----- frontend/mobile/src/app/auth/index.tsx | 77 +++++++++++-------- .../components/EventCard/EventCardList.tsx | 2 +- .../PointofContactsList.tsx | 2 +- .../components/Preview/Club/ClubPreview.tsx | 2 +- .../components/Preview/Event/EventPreview.tsx | 4 +- frontend/mobile/src/hooks/useEvent.ts | 1 - frontend/mobile/src/store/slices/userSlice.ts | 45 +++++++---- frontend/mobile/yarn.lock | 8 +- 25 files changed, 188 insertions(+), 186 deletions(-) diff --git a/backend/entities/auth/base/handlers.go b/backend/entities/auth/base/handlers.go index b5e39179a..c7a8d95a5 100644 --- a/backend/entities/auth/base/handlers.go +++ b/backend/entities/auth/base/handlers.go @@ -89,7 +89,7 @@ func (h *Handler) ProviderCallback(c *fiber.Ctx) error { return err } - return c.SendStatus(http.StatusOK) + return c.Status(http.StatusOK).JSON(user) } func (h *Handler) ProviderLogout(c *fiber.Ctx) error { diff --git a/backend/integrations/oauth/crypt/crypt.go b/backend/integrations/oauth/crypt/crypt.go index 52352c987..20d515ed5 100644 --- a/backend/integrations/oauth/crypt/crypt.go +++ b/backend/integrations/oauth/crypt/crypt.go @@ -17,7 +17,7 @@ func Encrypt(data string, passphrase string) (string, error) { } plaintext := []byte(data) - if len(plaintext) > 1028 { + if len(plaintext) > 4096 { return "", fmt.Errorf("plaintext too long") } diff --git a/backend/integrations/oauth/soth/sothic/sothic.go b/backend/integrations/oauth/soth/sothic/sothic.go index 9768c99b6..ff7c2a3f3 100644 --- a/backend/integrations/oauth/soth/sothic/sothic.go +++ b/backend/integrations/oauth/soth/sothic/sothic.go @@ -210,6 +210,7 @@ func CompleteUserAuth(c *fiber.Ctx) (soth.User, error) { } gu, err := provider.FetchUser(sess) + return gu, err } @@ -227,6 +228,7 @@ func validateState(c *fiber.Ctx, sess soth.Session) error { } originalState := authURL.Query().Get("state") + if originalState != "" && (originalState != c.Query("state")) { return errors.New("state token mismatch") } @@ -357,13 +359,10 @@ func updateSessionValue(session *session.Session, key, value string) error { if err := gz.Close(); err != nil { return err } - encrypted, err := encrypter(b.String()) if err != nil { return err } - session.Set(key, encrypted) - return nil } diff --git a/backend/middleware/auth/user.go b/backend/middleware/auth/user.go index fd5ee7277..e4d569bf8 100644 --- a/backend/middleware/auth/user.go +++ b/backend/middleware/auth/user.go @@ -17,10 +17,10 @@ func (m *AuthMiddlewareHandler) UserAuthorizeById(c *fiber.Ctx) error { return c.SendStatus(http.StatusUnauthorized) } - user := models.UnmarshalUser(strUser) + user := *models.UnmarshalUser(strUser) if user.Role == models.Super { - locals.SetUser(c, user) + locals.SetUser(c, &user) return c.Next() } @@ -29,8 +29,8 @@ func (m *AuthMiddlewareHandler) UserAuthorizeById(c *fiber.Ctx) error { return err } - if idAsUUID == &user.ID { - locals.SetUser(c, user) + if *idAsUUID == user.ID { + locals.SetUser(c, &user) return c.Next() } diff --git a/backend/server/server.go b/backend/server/server.go index 9cb952cba..76bdf3a99 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -58,7 +58,7 @@ func Init(db *gorm.DB, stores *store.Stores, integrations integrations.Integrati applicationURL := settings.Application.ApplicationURL() - msftProvider := msft.New(settings.Microsft.Key, "myapp://auth/callback", settings.Microsft.Tenant) + msftProvider := msft.New(settings.Microsft.Key, fmt.Sprintf("%s/api/v1/auth/microsoftonline/callback", applicationURL), settings.Microsft.Tenant) googProvider := goog.New(settings.Google.Key, settings.Google.Secret, fmt.Sprintf("%s/api/v1/auth/google/callback", applicationURL)) authMiddleware := authMiddleware.New( diff --git a/frontend/lib/package.json b/frontend/lib/package.json index cb1bacb53..d6006b1ae 100644 --- a/frontend/lib/package.json +++ b/frontend/lib/package.json @@ -1,6 +1,6 @@ { "name": "@generatesac/lib", - "version": "0.0.177", + "version": "0.0.179", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/frontend/lib/src/api/authApi.ts b/frontend/lib/src/api/authApi.ts index 6ebc26805..f45f4f9b8 100644 --- a/frontend/lib/src/api/authApi.ts +++ b/frontend/lib/src/api/authApi.ts @@ -1,12 +1,9 @@ -import { LoginResponse, RefreshTokenRequestBody } from "../types/auth"; -import { - EmailRequestBody, - VerifyEmailRequestBody, - VerifyPasswordResetTokenRequestBody, -} from "../types/verification"; -import { baseApi } from "./base"; +import { User, userSchema } from "../types"; +import { LoginResponse, OAuthCallbackRequestQueryParams } from "../types/auth"; +import { baseApi, handleQueryParams } from "./base"; const AUTH_API_BASE_URL = "/auth"; +const PROVIDER = "microsoftonline"; export const authApi = baseApi.injectEndpoints({ endpoints: (builder) => ({ @@ -26,49 +23,20 @@ export const authApi = baseApi.injectEndpoints({ } }, }), - logout: builder.mutation({ + logout: builder.query({ query: () => ({ url: `${AUTH_API_BASE_URL}/logout`, - method: "POST", - }), - }), - refresh: builder.mutation({ - query: (body) => ({ - url: "refresh", - method: "POST", - body, - }), - }), - forgotPassword: builder.mutation({ - query: (body) => ({ - url: `${AUTH_API_BASE_URL}/forgot-password`, - method: "POST", - body, - }), - }), - verifyPasswordResetToken: builder.mutation< - void, - VerifyPasswordResetTokenRequestBody - >({ - query: (body) => ({ - url: `${AUTH_API_BASE_URL}/verify-reset`, - method: "POST", - body, - }), - }), - sendCode: builder.mutation({ - query: (body) => ({ - url: `${AUTH_API_BASE_URL}/send-code`, - method: "POST", - body, + method: "GET", }), }), - verifyEmail: builder.mutation({ - query: (body) => ({ - url: `${AUTH_API_BASE_URL}/verify-email`, - method: "POST", - body, + callback: builder.query({ + query: (params) => ({ + url: handleQueryParams(`${AUTH_API_BASE_URL}/${PROVIDER}/callback`, params), + method: "GET", }), + transformResponse: (response) => { + return userSchema.parse(response); + } }), }), }); diff --git a/frontend/lib/src/api/base.ts b/frontend/lib/src/api/base.ts index 9a32ddfa9..a4dfb23a3 100644 --- a/frontend/lib/src/api/base.ts +++ b/frontend/lib/src/api/base.ts @@ -11,7 +11,7 @@ export const baseApi = createApi({ // User slice existing must exist in all dependent apps: const token = (getState() as { user: { accessToken: string } })?.user?.accessToken; if (token) { - headers.set("Authorization", `Bearer ${token}`); + headers.set("_sac_session", token); } return headers; }, diff --git a/frontend/lib/src/api/userApi.ts b/frontend/lib/src/api/userApi.ts index 766491a2e..e91c205c8 100644 --- a/frontend/lib/src/api/userApi.ts +++ b/frontend/lib/src/api/userApi.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -import { UpdatePasswordRequestBody } from "../types/auth"; import { Club, clubSchema } from "../types/club"; import { PaginationQueryParams } from "../types/root"; import { Tag, tagSchema } from "../types/tag"; @@ -82,16 +81,6 @@ export const userApi = baseApi.injectEndpoints({ }), invalidatesTags: (_result, _, id) => [{ type: "User", id }], }), - updatePassword: builder.mutation< - void, - { id: string; body: UpdatePasswordRequestBody } - >({ - query: ({ id, body }) => ({ - url: `${USER_API_BASE_URL}/${id}/password`, - method: "PATCH", - body, - }), - }), userFollowing: builder.query({ query: (id) => ({ url: `${USER_API_BASE_URL}/${id}/follower/`, diff --git a/frontend/lib/src/types/auth.ts b/frontend/lib/src/types/auth.ts index 0a5114fdb..c3eaec616 100644 --- a/frontend/lib/src/types/auth.ts +++ b/frontend/lib/src/types/auth.ts @@ -5,26 +5,12 @@ export const loginResponseSchema = z.object({ sac_session: z.string(), }); -export const updatePasswordRequestBodySchema = z.object({ - old_password: z.string().min(8), - new_password: z.string().min(8), -}); - -export const refreshTokenRequestBodySchema = z.object({ - refresh_token: z.string(), -}); - -export const tokensSchema = z.object({ - access_token: z.string(), - refresh_token: z.string(), +export const oauthCallbackRequestQueryParams = z.object({ + code: z.string(), + session_state: z.string(), + state: z.string(), }); // Types: export type LoginResponse = z.infer; -export type UpdatePasswordRequestBody = z.infer< - typeof updatePasswordRequestBodySchema ->; -export type RefreshTokenRequestBody = z.infer< - typeof refreshTokenRequestBodySchema ->; -export type Tokens = z.infer; +export type OAuthCallbackRequestQueryParams = z.infer; diff --git a/frontend/lib/src/types/user.ts b/frontend/lib/src/types/user.ts index b2e9b1572..cc18f52d8 100644 --- a/frontend/lib/src/types/user.ts +++ b/frontend/lib/src/types/user.ts @@ -6,6 +6,7 @@ import { rootModelSchema } from "./root"; export const userRoleEnum = z.enum(["super", "student"]); export const collegeEnum = z.enum([ + "", "CAMD", "DMSB", "KCCS", @@ -18,6 +19,7 @@ export const collegeEnum = z.enum([ ]); export const majorEnum = z.enum([ + "", "africanaStudies", "americanSignLanguage", "americanSignLanguage-EnglishInterpreting", @@ -120,7 +122,7 @@ export const majorEnum = z.enum([ "theatre", ]); -export const graduationCycleEnum = z.enum(["december", "may"]); +export const graduationCycleEnum = z.enum(["december", "may", ""]); export const yearEnum = z.enum(["1", "2", "3", "4", "5"]); @@ -155,8 +157,7 @@ export const createUserTagsRequestBodySchema = z.object({ const userSchemaIntermediate = z.object({ role: userRoleEnum, - first_name: z.string().min(1), - last_name: z.string().min(1), + name: z.string().min(1), email: z.string().email(), major0: majorEnum.optional(), major1: majorEnum.optional(), @@ -164,7 +165,6 @@ const userSchemaIntermediate = z.object({ college: collegeEnum.optional(), graduation_cycle: graduationCycleEnum.optional(), graduation_year: z.number().optional(), - is_verified: z.boolean(), }); export const userSchema = userSchemaIntermediate.merge(rootModelSchema); diff --git a/frontend/mobile/package.json b/frontend/mobile/package.json index 37b600937..7829812e3 100644 --- a/frontend/mobile/package.json +++ b/frontend/mobile/package.json @@ -25,7 +25,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-native-fontawesome": "^0.3.2", - "@generatesac/lib": "0.0.177", + "@generatesac/lib": "0.0.179", "@gorhom/bottom-sheet": "^4.6.3", "@hookform/resolvers": "^3.4.2", "@react-native-async-storage/async-storage": "^1.23.1", diff --git a/frontend/mobile/src/app/_layout.tsx b/frontend/mobile/src/app/_layout.tsx index 4ee52f3b9..e412505d1 100644 --- a/frontend/mobile/src/app/_layout.tsx +++ b/frontend/mobile/src/app/_layout.tsx @@ -20,7 +20,7 @@ export { ErrorBoundary } from 'expo-router'; SplashScreen.preventAutoHideAsync(); const InitalLayout = () => { - const { accessToken } = useAppSelector((state) => state.user); + const { accessToken, loggedIn } = useAppSelector((state) => state.user); const { eventPreviewRef, eventId, @@ -33,12 +33,13 @@ const InitalLayout = () => { } = usePreview(); useEffect(() => { - if (accessToken) { + if (accessToken && loggedIn) { router.push('/app/'); } else { router.push('/auth/'); } - }, [accessToken]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loggedIn]); return ( <> diff --git a/frontend/mobile/src/app/app/(tabs)/calendar.tsx b/frontend/mobile/src/app/app/(tabs)/calendar.tsx index d865b1fdd..9848bef41 100644 --- a/frontend/mobile/src/app/app/(tabs)/calendar.tsx +++ b/frontend/mobile/src/app/app/(tabs)/calendar.tsx @@ -6,6 +6,8 @@ import { } from 'react-native-calendars-sac'; import { Theme } from 'react-native-calendars-sac/src/types'; +import { eventApi } from '@generatesac/lib'; + import { Box, Colors, createStyles } from '@/src/app/design-system'; import Calendar, { DAY_EPOCH_TIME, @@ -13,7 +15,6 @@ import Calendar, { } from '@/src/app/design-system/components/Calendar/Calendar'; import { EventSection } from '@/src/app/design-system/components/Calendar/DayTimeSection'; import { parseData } from '@/src/app/design-system/components/Calendar/parser/calendarParser'; -import { eventApi } from '@generatesac/lib'; const TODAY = new Date(); diff --git a/frontend/mobile/src/app/app/(tabs)/profile.tsx b/frontend/mobile/src/app/app/(tabs)/profile.tsx index a1bdd8af0..5a79d0335 100644 --- a/frontend/mobile/src/app/app/(tabs)/profile.tsx +++ b/frontend/mobile/src/app/app/(tabs)/profile.tsx @@ -11,9 +11,10 @@ import { faUser } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; -import { Avatar } from '@rneui/base'; import { GlobalLayout } from '@/src/app/design-system/components/GlobalLayout/GlobalLayout'; +import { resetAccessToken } from '@/src/store/slices/userSlice'; +import { useAppDispatch, useAppSelector } from '@/src/store/store'; import { Box, Colors, SACColors, Spacing, Text } from '../../design-system'; @@ -43,6 +44,9 @@ const ProfileItem = ({ icon, text, textColor, onPress }: ProfileItemProps) => ( ); const ProfilePage = () => { + const dispatch = useAppDispatch(); + const user = useAppSelector((state) => state.user); + return ( @@ -52,27 +56,20 @@ const ProfilePage = () => { gap="m" alignItems="center" > - - Quokka - quokka@northeastern.edu + {user.name} + {user.email} router.push('/user/detail/')} + onPress={() => router.push('/app/user/detail/')} icon={faUser} text="Edit Profile" /> router.push('/user/interest/')} + onPress={() => router.push('/app/user/interest/')} text="Edit Interests" /> { icon={faSignOutAlt} text="Logout" textColor="darkRed" + onPress={() => { + dispatch(resetAccessToken()); + }} /> diff --git a/frontend/mobile/src/app/app/event/components/upcoming-events.tsx b/frontend/mobile/src/app/app/event/components/upcoming-events.tsx index d4253a1f1..02f7c1f8b 100644 --- a/frontend/mobile/src/app/app/event/components/upcoming-events.tsx +++ b/frontend/mobile/src/app/app/event/components/upcoming-events.tsx @@ -15,7 +15,7 @@ interface UpcomingEventsProps { export const UpcomingEvent: React.FC = ({ events }) => { const renderEventCard = ({ item }: { item: Event }) => { return ( - router.push(`/event/${item.id}`)}> + router.push(`/app/event/${item.id}`)}> { const params = useLocalSearchParams(); - const { sacLoginSession } = useAppSelector((state) => state.user); + const [oAuthCallback] = authApi.useLazyCallbackQuery(); + const dispatch = useAppDispatch(); + const { accessToken } = useAppSelector((state) => state.user); useEffect(() => { - const { code, session_state, state } = params; - console.log(params) - console.log(sacLoginSession); - if (!code || !session_state || !state) { - console.log('failed!') + const { code, session_state, state } = + params as OAuthCallbackRequestQueryParams; + if (code || session_state || state) { + oAuthCallback({ code, session_state, state }).then( + ({ data, error }) => { + if (data) { + dispatch(setUser(data)); + dispatch(setLoggedInState(true)); + router.push('/app/'); + } + if (error) { + router.push('/auth/'); + } + } + ); } - - }, [params]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accessToken]); return ( - - - Callback!!!!!!! - + + + Logging you in... - ) -} + ); +}; -export default OAuthCallback; \ No newline at end of file +export default OAuthCallback; diff --git a/frontend/mobile/src/app/auth/index.tsx b/frontend/mobile/src/app/auth/index.tsx index 07080c212..6daacdef8 100644 --- a/frontend/mobile/src/app/auth/index.tsx +++ b/frontend/mobile/src/app/auth/index.tsx @@ -1,61 +1,78 @@ -import React, { useEffect } from 'react'; - -import { Box, Button, Text } from '../design-system'; +import React from 'react'; import { Image, Linking, SafeAreaView } from 'react-native'; -import { GlobalLayout } from '../design-system/components/GlobalLayout/GlobalLayout'; + +import { router } from 'expo-router'; + import { authApi } from '@generatesac/lib'; + import Loading from '@/src/assets/gif/loading.gif'; -import { useLocalSearchParams } from 'expo-router'; +import { setAccessToken } from '@/src/store/slices/userSlice'; import { useAppDispatch } from '@/src/store/store'; -import { setSacLoginSession } from '@/src/store/slices/userSlice'; + +import { Box, Button, Text } from '../design-system'; +import { GlobalLayout } from '../design-system/components/GlobalLayout/GlobalLayout'; const WelcomePage = () => { - const [login, { isLoading, error, data }] = authApi.useLazyLoginQuery(); + const [login, { isLoading }] = authApi.useLazyLoginQuery(); const dispatch = useAppDispatch(); - const search = useLocalSearchParams(); - console.log(search); - const handleLogin = async () => { - await login().then(async () => { + const handleLogin = () => { + login().then(async ({ data }) => { if (data) { - dispatch(setSacLoginSession(data.sac_session)); await Linking.openURL(data.redirect_uri); + dispatch(setAccessToken(data.sac_session)); } - }) + }); }; return ( - - - Welcome to the - - + Welcome to the + Student Activity Calendar - + diff --git a/frontend/mobile/src/app/design-system/components/EventCard/EventCardList.tsx b/frontend/mobile/src/app/design-system/components/EventCard/EventCardList.tsx index 2c64bc4ec..2475f46e4 100644 --- a/frontend/mobile/src/app/design-system/components/EventCard/EventCardList.tsx +++ b/frontend/mobile/src/app/design-system/components/EventCard/EventCardList.tsx @@ -17,7 +17,7 @@ export const EventCardList: React.FC = ({ }) => { const renderEventCard = ({ item }: { item: Event }) => { return ( - router.push(`/event/${item.id}`)}> + router.push(`/app/event/${item.id}`)}> = ({ }) => { const renderPointOfContact = ({ item }: { item: PointOfContact }) => { return ( - router.push(`/contact/${item.id}`)}> + router.push(`/app/contact/${item.id}`)}> (