react-keycloak / react-native-keycloak

React Native components for Keycloak
MIT License
171 stars 48 forks source link

Unable to Redirect to Keycloak Login Page #124

Open Teut2711 opened 1 year ago

Teut2711 commented 1 year ago

I'm currently using the package in the following manner. My homepage consists of a single image that triggers a mock API call and should navigate to the login page. The login page contains username and password fields. I'm using the latest version of Keycloak deployed at my website https://kk.irasus.com/.

The issue I'm facing is that instead of being redirected to the Keycloak login page, I'm redirected to my custom login page. It seems that wrapping my login page component with the provided Keycloak component has no effect, as I still see my custom login page. My intention is to replace my custom login page with the Keycloak login page. Removing the form on the login page is planned once Keycloak integration works.

Could you please assist me in resolving this issue and ensuring that I'm correctly redirected to the Keycloak login page?

Thank you!

import React, {useState} from 'react';
import {StyleSheet, View} from 'react-native';
import {
  RNKeycloak,
  ReactNativeKeycloakProvider,
  useKeycloak,
} from '@react-keycloak/native';

import {HStack} from '@react-native-material/core';
import {Text, Switch} from 'react-native-paper';
import {VStack} from '@react-native-material/core';
import {TouchableOpacity} from 'react-native';

import {SubmitButton} from '~/__common__/components/Button';
import AuthProviders from '~/__common__/components/AuthProviders';
import Fields from '~/login/components/Fields';

const Authenticate = ({children}) => {
  const keycloakConfig = new RNKeycloak({
    url: 'https://kk.irasus.com',
    realm: 'master',
    clientId: 'esselenergy',
  });

  const keycloakInitOptions = {
    flow: 'implicit',
    // if you need to customize "react-native-inappbrowser-reborn" View you can use the following attribute
    inAppBrowserOptions: {},
    onLoad: 'check-sso',
    redirectUri: 'energymetrics://metricsdisplay',
    useNonce: true,
  };
  React.useEffect(() => {
    console.log(keycloakConfig);
  });
  return (
    <ReactNativeKeycloakProvider
      authClient={keycloakConfig}
      initOptions={keycloakInitOptions}>
      {children}
    </ReactNativeKeycloakProvider>
  );
};

const Login = ({navigation}) => {
  const [isEnabled, setIsEnabled] = useState(false);
  const toggleSwitch = () => setIsEnabled(previousState => !previousState);

  const [keycloak, initialized] = useKeycloak();
  const handleLogin = e => {
    // if (keycloak.authenticated) {
    //   console.log('Authenticated');
    //   navigation.navigate('VEHICLEIDENTITYINFO');
    // } else {
    //   console.log('Not Authenticated');
    //   console.log(keycloak.authServerUrl);
    // }
  };

  return (
    <Authenticate>
      <VStack fill center p={10} spacing={10}>
        <Text variant="displaySmall">Log In</Text>
        <Fields />
        <HStack m={4} spacing={30}>
          <HStack m={4} spacing={6}>
            <Switch
              trackColor={{false: '#E0E0E0', true: '#4CAF50'}}
              thumbColor={isEnabled ? '#FFFFFF' : '#FFFFFF'}
              ios_backgroundColor="#9E9E9E"
              onValueChange={toggleSwitch}
              value={isEnabled}
            />
            <Text>Remember me</Text>
          </HStack>
          <HStack m={4} spacing={6}>
            <Text style={styles.underline}>Forgot Password?</Text>{' '}
          </HStack>
        </HStack>
        <SubmitButton title="Login" onPress={handleLogin} />
        <HStack spacing={5}>
          <View style={styles.divider} />
          <Text>Or login with</Text>
          <View style={styles.divider} />
        </HStack>
        <AuthProviders navigation={navigation} />
        <HStack m={4} spacing={6}>
          <Text>Don’t have an account?</Text>
          <TouchableOpacity
            onPress={() => navigation.navigate('SIGNUP')}
            style={styles.accessApp}>
            <Text style={styles.accessThrough}>Signup</Text>
          </TouchableOpacity>
        </HStack>
        <HStack m={4} spacing={6}>
          <Text>Want to login with OTP?</Text>
          <TouchableOpacity
            onPress={() => navigation.navigate('OTP')}
            style={styles.accessApp}>
            <Text style={styles.accessThrough}>Click here</Text>
          </TouchableOpacity>
        </HStack>
      </VStack>
    </Authenticate>
  );
};

const styles = StyleSheet.create({
  underline: {textDecorationLine: 'underline'},

  divider: {
    flex: 1,
    borderBottomWidth: 1,
    borderBottomColor: '#A09F99',
    marginBottom: 9,
  },
  accessApp: {
    borderRadius: 2,
    backgroundColor: 'rgba(0,0,0,0)',
    alignSelf: 'flex-start',
  },
  accessThrough: {
    color: '#2FAD65',
    fontWeight: 'bold',
  },
});

export default Login;
viabells-winko commented 1 year ago

Have you found a solution?.

Teut2711 commented 1 year ago

Ya, I have, remove the package.

import React, {useEffect, useState} from 'react';
import {NODE_ENV} from '@env';
import WebView from 'react-native-webview';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {Linking} from 'react-native';
import {useRoute, RouteProp, useNavigation} from '@react-navigation/native';
import {ActivityIndicator} from 'react-native-paper';
import jwtDecode from 'jwt-decode';
import {VStack} from '@react-native-material/core';
import {asyncStoreSetWrapper} from 'asyncStorageWrappers';
interface IClientAttrs {
  id: string;
  secret: string;
  loginRedirectTo: URL;
  logoutRedirectTo: URL;
  scopes: string;
}

interface IRequestOptions {
  method: string;
  headers: Headers;
  body: string;
  redirect: string;
}

interface IKeycloakAuth {
  keycloakBaseUrl: URL;
  realmName: string;
  clientAttrs: IClientAttrs;
}

interface IWebViewState {
  url: string;
  [key: string]: any;
}

function extractCodeFromUrl(url: string): string | null {
  const regex = /[?&]code=([^&#]*)/i;
  const match = regex.exec(url as string);
  if (match) {
    return decodeURIComponent(match[1]);
  }
  console.log('Could not retrive code from url: ' + url);
  return null;
}

async function getAcessToken(
  keycloakBaseUrl: URL,
  realmName: string,
  client: IKeycloakAuth['clientAttrs'],
  code: string,
) {
  const myHeaders: Headers = new Headers();
  myHeaders.append('Content-Type', 'application/x-www-form-urlencoded');

  const urlencoded: URLSearchParams = new URLSearchParams();
  urlencoded.append('grant_type', 'authorization_code');
  urlencoded.append('client_id', client.id);
  urlencoded.append('client_secret', client.secret);
  urlencoded.append('code', code.toString());
  urlencoded.append(
    'redirect_uri',
    client.loginRedirectTo as unknown as string,
  );

  const requestOptions: IRequestOptions = {
    method: 'POST',
    headers: myHeaders,
    body: (urlencoded as URLSearchParams).toString(),
    redirect: 'follow',
  };
  try {
    const response = await fetch(
      `${keycloakBaseUrl}/realms/${realmName}/protocol/openid-connect/token`,
      requestOptions,
    );
    const result = await response.json();
    return result;
  } catch (error) {
    console.log('Could not receive access token: ', error);
  }
}
type ParamList = {
  KEYCLOAK: {
    logout: boolean;
  };
};

const KeycloakAuth = ({
  keycloakBaseUrl,
  realmName,
  clientAttrs,
}: IKeycloakAuth) => {
  const {params}: RouteProp<ParamList, 'KEYCLOAK'> = useRoute();
  const navigation = useNavigation();
  const handleOnShouldStartLoadWithRequest = (event: any) => {
    if (
      event.url.startsWith(clientAttrs.loginRedirectTo as unknown as string) ||
      event.url.startsWith(clientAttrs.logoutRedirectTo as unknown as string)
    ) {
      Linking.openURL(event.url);
      return false;
    } else {
      return true;
    }
    //TODO: Remove this when deep links work properly
  };
  const handleWebViewStateChange = async (webViewState: IWebViewState) => {
    if (params?.logout) {
      return;
    }
    try {
      //TODO: Improve error handling
      const {url}: {url: string} = webViewState;

      if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
        const code = extractCodeFromUrl(url);
        if (code !== null) {
          let authResponse;
          authResponse = await getAcessToken(
            keycloakBaseUrl,
            realmName,
            clientAttrs,
            code,
          );
          if (NODE_ENV === 'production') {
            console.info('Auth response from keycloak is :', authResponse);
          }
          await asyncStoreSetWrapper('keycloak', authResponse);
          if (authResponse.id_token) {
            const userInfo = jwtDecode(authResponse.id_token) as Record<
              string,
              any
            >;
            await asyncStoreSetWrapper('user', userInfo);
          }
        }
      }
    } catch (error) {
      console.log('Error getting code: ', error);
    }
  };
  const [authUrl, setAuthUrl] = useState<null | string>(null);

  useEffect(() => {
    const getAuthUrl = async () => {
      let url;
      if (params?.logout) {
        url = getLogoutURL(keycloakBaseUrl, realmName, clientAttrs);
      } else {
        url = getLoginURL(keycloakBaseUrl, realmName, clientAttrs);
      }
      setAuthUrl(url);
    };

    getAuthUrl();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [params?.logout]);

  if (authUrl === null) {
    return (
      <VStack fill center style={{width: '100%', height: '100%'}}>
        <ActivityIndicator animating={true} color="#990000" />
      </VStack>
    );
  }
  //not required as of now since idToke is being parsed to get user identity
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const userInfoEndpoint = `${keycloakBaseUrl}/realms/${realmName}/protocol/openid-connect/userInfo`;

  return (
    <WebView
      source={{
        uri: authUrl as string,
      }}
      style={{flex: 1}}
      onNavigationStateChange={handleWebViewStateChange}
      javaScriptEnabled
      sharedCookiesEnabled={true}
      onShouldStartLoadWithRequest={handleOnShouldStartLoadWithRequest}
    />
  );
};
export default KeycloakAuth;

function getLoginURL(
  keycloakBaseUrl: URL,
  realmName: string,
  clientAttrs: IClientAttrs,
) {
  const responseType = 'code'; // Use 'code' for Authorization Code flow
  const authEndPoint = `${keycloakBaseUrl}/realms/${realmName}/protocol/openid-connect/auth`;
  const authUrl = `${authEndPoint}?client_id=${
    clientAttrs.id
  }&redirect_uri=${encodeURIComponent(
    clientAttrs.loginRedirectTo as unknown as string,
  )}&response_type=${responseType}&scope=${encodeURIComponent(
    clientAttrs.scopes,
  )}`;
  return authUrl;
}

function getLogoutURL(
  keycloakBaseUrl: URL,
  realmName: string,
  clientAttrs: IClientAttrs,
) {
  const authEndPoint = `${keycloakBaseUrl}/realms/${realmName}/protocol/openid-connect/logout`;
  const authUrl = `${authEndPoint}?client_id=${
    clientAttrs.id
  }&post_logout_redirect_uri =${encodeURIComponent(
    clientAttrs.logoutRedirectTo as unknown as string,
  )}`;

  return authUrl;
}
 originalProps: {
        keycloakBaseUrl: KEYCLOAK_BASE_URL,
        realmName: KEYCLOAK_REALM_NAME,
        clientAttrs: {
          id: KEYCLOAK_CLIENT_ID,
          secret: KEYCLOAK_CLIENT_SECRET,
          loginRedirectTo: KEYCLOAK_LOGIN_REDIRECT_TO,
          logoutRedirectTo: KEYCLOAK_LOGOUT_REDIRECT_TO,
          scopes: 'openid',
          //KEYCLOAK_SCOPES,
        },
      }

This is a react native component that works for me, similar you can do for any app.

viabells-winko commented 1 year ago

Thank you for providing your code. I'll try to test