Skip to content

Commit 035c087

Browse files
Phase 5 - Implement OAuth Social Login with Cognito and Redirect Handling
- Add OAuth redirect handler for social login (Google, Apple) - Create SocialLoginButtons component with styling - Update Cognito configuration for OAuth flows - Enhance authentication service to support federated sign-in - Add OAuth-related translations and error handling - Update AppRouter to include OAuth redirect route
1 parent e5f96cf commit 035c087

File tree

9 files changed

+285
-96
lines changed

9 files changed

+285
-96
lines changed

frontend/src/common/components/Router/AppRouter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import SignInPage from 'pages/Auth/SignIn/SignInPage';
88
import SignUpPage from 'pages/Auth/SignUp/SignUpPage';
99
import SignOutPage from 'pages/Auth/SignOut/SignOutPage';
1010
import VerificationPage from 'pages/Auth/Verify/VerificationPage';
11+
import OAuthRedirectHandler from 'pages/Auth/OAuth/OAuthRedirectHandler';
1112

1213
/**
1314
* The application router. This is the main router for the Ionic React
@@ -33,6 +34,7 @@ const AppRouter = (): JSX.Element => {
3334
<Route exact path="/auth/signin" render={() => <SignInPage />} />
3435
<Route exact path="/auth/signup" render={() => <SignUpPage />} />
3536
<Route exact path="/auth/verify" render={() => <VerificationPage />} />
37+
<Route exact path="/auth/oauth" render={() => <OAuthRedirectHandler />} />
3638
<Route exact path="/auth/signout" render={() => <SignOutPage />} />
3739
<Route exact path="/">
3840
<Redirect to="/tabs" />
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.ls-social-login-buttons {
2+
margin-top: 1rem;
3+
4+
&__google {
5+
--background-hover: #f2f2f2;
6+
--color-hover: #4285f4;
7+
--border-color: #dddddd;
8+
}
9+
10+
&__apple {
11+
--background-hover: #000000;
12+
--color-hover: #ffffff;
13+
--border-color: #333333;
14+
}
15+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { IonButton, IonRow, IonCol, IonText } from '@ionic/react';
2+
import { useTranslation } from 'react-i18next';
3+
import './SocialLoginButtons.scss';
4+
5+
import { BaseComponentProps } from '../types';
6+
import Icon from '../Icon/Icon';
7+
import { useSocialSignIn } from 'common/hooks/useAuth';
8+
9+
/**
10+
* Properties for the `SocialLoginButtons` component.
11+
*/
12+
interface SocialLoginButtonsProps extends BaseComponentProps {
13+
disabled?: boolean;
14+
}
15+
16+
/**
17+
* A component that renders social login buttons (Google, Apple)
18+
* @param {SocialLoginButtonsProps} props - Component properties.
19+
* @returns {JSX.Element} JSX
20+
*/
21+
const SocialLoginButtons = ({
22+
className,
23+
disabled = false,
24+
testid = 'social-login-buttons'
25+
}: SocialLoginButtonsProps): JSX.Element => {
26+
const { t } = useTranslation();
27+
const { signInWithGoogle, signInWithApple, isLoading } = useSocialSignIn();
28+
29+
const isButtonDisabled = disabled || isLoading;
30+
31+
return (
32+
<div className={`ls-social-login-buttons ${className || ''}`} data-testid={testid}>
33+
<IonRow className="ion-text-center ion-padding">
34+
<IonCol>
35+
<IonText color="medium">
36+
{t('or-signin-with', { ns: 'auth' })}
37+
</IonText>
38+
</IonCol>
39+
</IonRow>
40+
41+
<IonRow>
42+
<IonCol>
43+
<IonButton
44+
expand="block"
45+
fill="outline"
46+
color="medium"
47+
onClick={signInWithGoogle}
48+
disabled={isButtonDisabled}
49+
data-testid={`${testid}-button-google`}
50+
className="ls-social-login-buttons__google"
51+
>
52+
<Icon icon="google" slot="start" />
53+
Google
54+
</IonButton>
55+
</IonCol>
56+
<IonCol>
57+
<IonButton
58+
expand="block"
59+
fill="outline"
60+
color="dark"
61+
onClick={signInWithApple}
62+
disabled={isButtonDisabled}
63+
data-testid={`${testid}-button-apple`}
64+
className="ls-social-login-buttons__apple"
65+
>
66+
<Icon icon="apple" slot="start" />
67+
Apple
68+
</IonButton>
69+
</IonCol>
70+
</IonRow>
71+
</div>
72+
);
73+
};
74+
75+
export default SocialLoginButtons;

frontend/src/common/config/aws-config.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,29 @@
88
export const REGION = 'us-east-1'; // Replace with your AWS region
99

1010
/**
11-
* Cognito Configuration
11+
* App configuration
12+
*/
13+
export const APP_CONFIG = {
14+
// App URLs for OAuth redirects
15+
localRedirectSignIn: import.meta.env.VITE_REDIRECT_SIGN_IN || 'http://localhost:3000/',
16+
localRedirectSignOut: import.meta.env.VITE_REDIRECT_SIGN_OUT || 'http://localhost:3000/',
17+
18+
// For deployed environments, these would be different URLs
19+
hostedRedirectSignIn: import.meta.env.VITE_HOSTED_REDIRECT_SIGN_IN || 'https://yourapp.com/',
20+
hostedRedirectSignOut: import.meta.env.VITE_HOSTED_REDIRECT_SIGN_OUT || 'https://yourapp.com/',
21+
};
22+
23+
/**
24+
* Auth/Cognito Configuration
1225
*/
1326
export const COGNITO_CONFIG = {
1427
// User Pool
1528
USER_POOL_ID: import.meta.env.VITE_COGNITO_USER_POOL_ID || 'us-east-1_xxxxxxxx', // Replace with your User Pool ID
1629
USER_POOL_WEB_CLIENT_ID: import.meta.env.VITE_COGNITO_APP_CLIENT_ID || 'xxxxxxxxxxxxxxxxxxxxxxxxxx', // Replace with your App Client ID
1730

1831
// OAuth Configuration (for Social Login)
19-
OAUTH: {
20-
domain: import.meta.env.VITE_COGNITO_DOMAIN || 'your-domain.auth.us-east-1.amazoncognito.com', // Replace with your Cognito domain
21-
scope: ['email', 'profile', 'openid'],
22-
redirectSignIn: import.meta.env.VITE_REDIRECT_SIGN_IN || 'http://localhost:3000/',
23-
redirectSignOut: import.meta.env.VITE_REDIRECT_SIGN_OUT || 'http://localhost:3000/',
24-
responseType: 'code' as const
25-
},
32+
OAUTH_DOMAIN: import.meta.env.VITE_COGNITO_DOMAIN || 'your-domain.auth.us-east-1.amazoncognito.com', // Replace with your Cognito domain
33+
OAUTH_SCOPES: ['email', 'profile', 'openid'],
2634

2735
// Auth mechanisms
2836
AUTH_MECHANISMS: ['EMAIL'],
@@ -31,6 +39,22 @@ export const COGNITO_CONFIG = {
3139
SOCIAL_PROVIDERS: ['Google', 'SignInWithApple'],
3240
};
3341

42+
/**
43+
* Get redirect URLs for OAuth based on the environment
44+
* In development, we use localhost, in production we use the hosted URLs
45+
*/
46+
const redirectUrls = {
47+
// For local development
48+
signIn: [APP_CONFIG.localRedirectSignIn],
49+
signOut: [APP_CONFIG.localRedirectSignOut],
50+
};
51+
52+
// If we're in a production-like environment, add the hosted URLs
53+
if (import.meta.env.PROD) {
54+
redirectUrls.signIn.push(APP_CONFIG.hostedRedirectSignIn);
55+
redirectUrls.signOut.push(APP_CONFIG.hostedRedirectSignOut);
56+
}
57+
3458
/**
3559
* Amplify Configuration object for initializing Amplify (V6 format)
3660
*/
@@ -44,10 +68,10 @@ export const amplifyConfig = {
4468
phone: false,
4569
username: false,
4670
oauth: {
47-
domain: COGNITO_CONFIG.OAUTH.domain,
48-
scopes: COGNITO_CONFIG.OAUTH.scope,
49-
redirectSignIn: [COGNITO_CONFIG.OAUTH.redirectSignIn],
50-
redirectSignOut: [COGNITO_CONFIG.OAUTH.redirectSignOut],
71+
domain: COGNITO_CONFIG.OAUTH_DOMAIN,
72+
scopes: COGNITO_CONFIG.OAUTH_SCOPES,
73+
redirectSignIn: redirectUrls.signIn,
74+
redirectSignOut: redirectUrls.signOut,
5175
responseType: 'code' as const
5276
}
5377
}

frontend/src/common/services/auth/cognito-auth-service.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { signIn, signUp, confirmSignUp, signOut,
2-
fetchAuthSession, getCurrentUser, resendSignUpCode } from '@aws-amplify/auth';
2+
fetchAuthSession, getCurrentUser, resendSignUpCode, signInWithRedirect,
3+
type AuthUser } from '@aws-amplify/auth';
34
import { Amplify } from 'aws-amplify';
45
import { amplifyConfig } from '../../config/aws-config';
56
import { UserTokens } from '../../models/auth';
7+
import { AuthProvider } from '@aws-amplify/auth/dist/esm/types/inputs';
8+
9+
// Provider enum for OAuth
10+
enum OAuthProvider {
11+
Google = 'Google',
12+
Apple = 'Apple'
13+
}
614

715
/**
816
* Initialize AWS Amplify with the configuration
@@ -105,12 +113,12 @@ export class CognitoAuthService {
105113
* Get current authenticated user
106114
* @returns Promise resolving to the current authenticated user
107115
*/
108-
static async getCurrentUser() {
116+
static async getCurrentUser(): Promise<AuthUser | null> {
109117
try {
110118
return await getCurrentUser();
111-
} catch (_error) {
119+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
120+
} catch (_) {
112121
// Not throwing here as this is often used to check if a user is signed in
113-
console.error('Error getting current user:', _error);
114122
return null;
115123
}
116124
}
@@ -122,8 +130,8 @@ export class CognitoAuthService {
122130
static async getCurrentSession() {
123131
try {
124132
return await fetchAuthSession();
125-
} catch (_error) {
126-
console.error('Error getting current session:', _error);
133+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
134+
} catch (_) {
127135
return null;
128136
}
129137
}
@@ -152,26 +160,36 @@ export class CognitoAuthService {
152160
expires_in: Math.floor((new Date(expirationTime * 1000).getTime() - Date.now()) / 1000),
153161
expires_at: new Date(expirationTime * 1000).toISOString(),
154162
};
155-
} catch (_error) {
156-
this.handleAuthError(_error);
163+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
164+
} catch (_) {
157165
return null;
158166
}
159167
}
160168

161169
/**
162170
* Initiate social sign in (Google or Apple)
163171
* @param provider 'Google' or 'SignInWithApple'
164-
* @returns Promise
172+
* @returns Promise resolving to void (redirects to the IdP)
165173
*/
166-
static async federatedSignIn(provider: 'Google' | 'SignInWithApple') {
174+
static async federatedSignIn(provider: 'Google' | 'SignInWithApple'): Promise<void> {
167175
try {
168-
// This needs OAuth configuration in the Amplify setup
169-
// In AWS Amplify v6, we'd use a different approach for federated sign in
170-
// Placeholder for now
171-
console.warn('federatedSignIn is not implemented in this version', provider);
172-
// In production, federated sign-in would be handled differently
173-
// For example, using a hosted UI or custom implementation
174-
return null;
176+
// Map our provider names to Cognito's provider identifiers
177+
const providerMap: Record<string, string> = {
178+
'Google': OAuthProvider.Google,
179+
'SignInWithApple': OAuthProvider.Apple
180+
};
181+
182+
// Get the OAuth provider
183+
const oauthProvider = providerMap[provider];
184+
if (!oauthProvider) {
185+
throw new Error(`Unsupported provider: ${provider}`);
186+
}
187+
188+
// Initiate the OAuth redirect flow
189+
await signInWithRedirect({ provider: oauthProvider as AuthProvider });
190+
191+
// This function will redirect the browser and not return
192+
// The user will be redirected back to the app after authentication
175193
} catch (error) {
176194
this.handleAuthError(error);
177195
throw error;

frontend/src/common/utils/i18n/resources/en/auth.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"error": {
33
"unable-to-verify": "We were unable to verify your credentials. Please try again.",
4-
"no-email": "Email address is required for verification."
4+
"no-email": "Email address is required for verification.",
5+
"oauth-redirect": "There was an error processing your social login"
56
},
67
"info-username": {
78
"part1": "This example application uses ",
@@ -39,6 +40,13 @@
3940
"success": "Email verified successfully!",
4041
"code-resent": "A new verification code has been sent to your email."
4142
},
43+
"oauth": {
44+
"processing": "Processing your login...",
45+
"redirecting": "Redirecting...",
46+
"success": "Successfully signed in!",
47+
"google": "Continue with Google",
48+
"apple": "Continue with Apple"
49+
},
4250
"validation": {
4351
"numeric": "Must contain only numbers",
4452
"exact-length": "Must be exactly {{length}} characters",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.ls-oauth-handler {
2+
.ls-oauth-handler__content {
3+
height: 100%;
4+
display: flex;
5+
flex-direction: column;
6+
justify-content: center;
7+
align-items: center;
8+
text-align: center;
9+
padding: 2rem;
10+
}
11+
12+
.ls-oauth-handler__spinner {
13+
margin-top: 2rem;
14+
}
15+
}

0 commit comments

Comments
 (0)