diff --git a/components/auth/AzureSignIn.tsx b/components/auth/AzureSignIn.tsx index 99bbcc2..712e09e 100644 --- a/components/auth/AzureSignIn.tsx +++ b/components/auth/AzureSignIn.tsx @@ -1,102 +1,135 @@ import React, { useState } from 'react'; +import { StyleSheet, Alert } from 'react-native'; import * as WebBrowser from 'expo-web-browser'; -import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session'; -import { Platform, StyleSheet } from 'react-native'; +import * as Linking from 'expo-linking'; +import * as AuthSession from 'expo-auth-session'; +import * as QueryParams from 'expo-auth-session/build/QueryParams'; import { supabase } from '@/lib/supabase'; -import { ThemedView, ThemedTextButton } from '@/components/theme'; +import { ThemedView, ThemedButton, ThemedText } from '@/components/theme'; +import { Colors } from '@/constants/Colors'; -// This is important - it completes the auth session when the browser redirects back to your app WebBrowser.maybeCompleteAuthSession(); +// Configuration for Azure AD +const tenantId = process.env.EXPO_PUBLIC_AZURE_TENANT_ID; +const clientId = process.env.EXPO_PUBLIC_AZURE_CLIENT_ID; + +// Create MSAL auth request +const redirectUri = Linking.createURL('auth/callback'); +const discovery = { + authorizationEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`, + tokenEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, +}; + const AzureSignIn = () => { const [loading, setLoading] = useState(false); - - // Get environment variables - const tenantId = process.env.EXPO_PUBLIC_AZURE_TENANT_ID as string; - const clientId = process.env.EXPO_PUBLIC_AZURE_CLIENT_ID as string; - - // Set up the discovery endpoint for Azure AD - const discovery = useAutoDiscovery( - `https://login.microsoftonline.com/${tenantId}/v2.0` - ); - - // Create a redirect URI that matches what you configured in Azure - // This should match the redirect URI you set in your Supabase dashboard - const redirectUri = makeRedirectUri({ - scheme: Platform.OS === 'web' ? undefined : 'com.gbrown.techtracker', - path: Platform.OS === 'web' ? 'auth/callback' : undefined, - }); - - // Set up the auth request with the needed scopes - const [request, response, promptAsync] = useAuthRequest( - { - clientId, - scopes: ['openid', 'profile', 'email', 'offline_access'], - redirectUri, - responseType: 'code', // Important for Supabase - usePKCE: true, // Use PKCE for added security - }, - discovery - ); - + const signInWithAzure = async () => { try { setLoading(true); - - // For Expo Go and mobile apps, we need to use the Expo Auth Session flow - if (Platform.OS !== 'web') { - // Launch the browser for authentication - const result = await promptAsync(); - - if (result.type === 'success') { - // If we got an authorization code, use Supabase to exchange it for a session - const { code } = result.params; - - const { data, error } = await supabase.auth.exchangeCodeForSession(code); - - if (error) { - throw error; - } - - console.log('Successfully signed in with Azure!', data.user); - } else if (result.type === 'error') { - throw new Error(result.error?.message || 'Authentication failed'); - } else if (result.type === 'cancel') { - console.log('User cancelled the login flow'); + console.log('Starting Azure sign-in with tenant-specific endpoint'); + console.log('Redirect URI:', redirectUri); + + // Create the MSAL auth request + const request = new AuthSession.AuthRequest({ + clientId: clientId!, + scopes: ['openid', 'profile', 'email', 'offline_access', 'User.Read'], + redirectUri, + usePKCE: true, + responseType: AuthSession.ResponseType.Code, + }); + + // Generate the auth URL with PKCE + const authUrl = await request.makeAuthUrlAsync(discovery); + console.log('Generated auth URL:', authUrl); + + // Open the auth URL in a browser + const result = await WebBrowser.openAuthSessionAsync( + authUrl, + redirectUri, + { + showInRecents: true, } - } else { - // For web, we can use Supabase's built-in OAuth flow - const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'azure', - options: { - scopes: 'email profile openid offline_access', - redirectTo: window.location.origin + '/auth/callback', + ); + + console.log('Auth session result type:', result.type); + + if (result.type === 'success' && result.url) { + // Parse the URL to get the authorization code + const { params, errorCode } = QueryParams.getQueryParams(result.url); + + if (errorCode || params.error) { + const errorMessage = params.error_description || params.error || errorCode; + throw new Error(`Error during authentication: ${errorMessage}`); + } + + if (!params.code) { + throw new Error('No authorization code received'); + } + + console.log('Authorization code received'); + + // Exchange the code for tokens + const tokenResult = await AuthSession.exchangeCodeAsync( + { + clientId: clientId!, + code: params.code, + redirectUri, + extraParams: { + code_verifier: request.codeVerifier || '', + }, }, + discovery + ); + + console.log('Token exchange successful'); + + if (!tokenResult.idToken) { + throw new Error('No ID token received'); + } + + // Now use the ID token to sign in with Supabase + const { data, error } = await supabase.auth.signInWithIdToken({ + provider: 'azure', + token: tokenResult.idToken, }); if (error) { + console.error('Supabase sign-in error:', error); throw error; } + + console.log('Successfully signed in with Azure via Supabase'); + return data; + } else { + console.log('Authentication was canceled or failed'); } - } catch (error) { + } catch (error: any) { console.error('Error signing in with Azure:', error); - alert(`Error signing in: ${error.message}`); + Alert.alert('Sign In Error', error.message || 'An error occurred during sign in'); } finally { setLoading(false); } }; - + return ( - + > + + {loading ? "Signing in..." : "Sign in with Microsoft"} + + ); }; + export default AzureSignIn; const styles = StyleSheet.create({ @@ -106,4 +139,4 @@ const styles = StyleSheet.create({ alignItems: 'center', marginTop: 20, }, -}); +}); diff --git a/package-lock.json b/package-lock.json index 011572d..af9fe08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "52.0.59", "license": "0BSD", "dependencies": { + "@azure/msal-browser": "^4.5.1", + "@azure/msal-react": "^3.0.5", "@expo/metro-runtime": "~4.0.1", "@expo/ngrok": "4.1.0", "@expo/vector-icons": "^14.0.2", @@ -39,6 +41,7 @@ "expo-updates": "~0.26.13", "expo-web-browser": "~14.0.2", "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.6", @@ -89,6 +92,40 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/msal-browser": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.5.1.tgz", + "integrity": "sha512-vcva6qA4ytVjg52Ew+RxXGKRuoDMdvNOwT+kECNC36kujYalFQe9B5SNud4WVa/Zk12KFa0bkOHFnjP8cgDv3A==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.2.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz", + "integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-react": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.5.tgz", + "integrity": "sha512-TrkExiYuytgDnEX53Rfq02GeZXo7sCQ2isK9PJpR+5kZ+9nuhKnUjV4BL+KKF69RdkmRTcgqaOB6dOH3xlrO0Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@azure/msal-browser": "^4.3.0", + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -10869,6 +10906,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index 17fbdaa..b02224b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,10 @@ "preset": "jest-expo" }, "dependencies": { + "@azure/msal-browser": "^4.5.1", + "@azure/msal-react": "^3.0.5", "@expo/metro-runtime": "~4.0.1", + "@expo/ngrok": "4.1.0", "@expo/vector-icons": "^14.0.2", "@react-native-async-storage/async-storage": "^1.23.1", "@react-navigation/bottom-tabs": "^7.2.0", @@ -27,13 +30,17 @@ "aes-js": "^3.1.2", "expo": "~52.0.28", "expo-apple-authentication": "~7.1.3", + "expo-auth-session": "~6.0.3", "expo-blur": "~14.0.3", "expo-constants": "~17.0.7", "expo-dev-client": "~5.0.12", + "expo-device": "~7.0.2", "expo-font": "~13.0.3", "expo-haptics": "~14.0.1", "expo-insights": "~0.8.2", "expo-linking": "~7.0.5", + "expo-location": "~18.0.7", + "expo-notifications": "~0.29.13", "expo-router": "~4.0.17", "expo-secure-store": "~14.0.1", "expo-splash-screen": "~0.29.21", @@ -43,6 +50,7 @@ "expo-updates": "~0.26.13", "expo-web-browser": "~14.0.2", "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.6", @@ -52,12 +60,7 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-web": "~0.19.13", - "react-native-webview": "13.12.5", - "@expo/ngrok": "4.1.0", - "expo-notifications": "~0.29.13", - "expo-device": "~7.0.2", - "expo-location": "~18.0.7", - "expo-auth-session": "~6.0.3" + "react-native-webview": "13.12.5" }, "devDependencies": { "@babel/core": "^7.25.2",