I have an Angular/Ionic/Capacitor app that uses the ionic-appauth library to handle it’s okta authentication. That library also uses the capacitor secure-storage plugin to store the okta access token locally.
I’m wanting to replace this app with an entirely new app (but via an App update through the store rather than a new app install). I’d like users to remain authenticated when they update the app so they do not have to remember their credentials and sign in when the app updates.
The updated app is a React / Capacitor app which uses the okta-auth-js library. I have added the capacitor-secure-storage plugin to this app just to get the token on startup.
To test the above 'update and stay authenticated" task out I have a button displayed on screen which makes a test call. When the app opens up I retrieve the access token from storage and make the test call - the call works fine. It continues to work ok until this token expires (1 hour) and although in my code I try and refresh the token at this point, the call still fails (401) and the login screen appears.
Asking CoPilot for help I added a migrateToken method in case the issue was coming from an app that uses the ionic-appauth library which uses @openid/appauth to my updated version which uses okyta-auth-js. But adding this method and called it on startup has not had any affect.
Is what I am trying to do possible? (I would have thought it would be)
Is there anyything I am missing around tokens and refreshing that I need to do in the updated app?
I have checked the okta config and it is in the same in both apps i.e the same issuer, clientId, redirectUri, scopes and pkce setting.
After 1 hour when I try the call I see the following console messages:
[log] - Access token expired. Attempting to refresh.
⚡️ [error] - Failed to refresh token: {"name":"AuthSdkError","errorCode":"INTERNAL","errorSummary":"renewTokens: invalid tokens: could not read authorizeUrl","errorLink":"INTERNAL","errorId":"INTERNAL","errorCauses":[],"tokenKey":"accessToken"}
⚡️ [log] - Redirecting to login...
Here is my test code in App.jsx of my updated app:
import React, { useEffect } from 'react';
import { SplashScreen } from '@capacitor/splash-screen';
import { SecureStoragePlugin } from 'capacitor-secure-storage-plugin';
import fetcher from 'services/fetcher';
import { OktaAuth } from '@okta/okta-auth-js';
const oktaAuth = new OktaAuth({
issuer: 'https://mydomain/oauth2/default',
clientId: 'xxxxxxxxxxxxxxxxxxxxxx',
redirectUri: 'mydomain:/callback',
scopes: ['openid', 'offline_access'],
pkce: true,
});
const App = () => {
const migrateTokens = async () => {
const { value: oldTokenResponse } = await SecureStoragePlugin.get({
key: 'token_response',
});
if (oldTokenResponse) {
const parsedTokenResponse = JSON.parse(oldTokenResponse);
const accessToken = parsedTokenResponse?.access_token;
const refreshToken = parsedTokenResponse?.refresh_token;
if (accessToken && refreshToken) {
const tokenToStore = {
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: Math.floor(Date.now() / 1000) + 3600, // Assume 1-hour expiry
scopes: ['openid', 'profile', 'offline_access'],
};
oktaAuth.tokenManager.add('accessToken', tokenToStore);
console.log('Tokens migrated successfully.');
}
}
};
const initializeAuth = async () => {
try {
// Retrieve the token from Secure Storage
const { value: tokenResponse } = await SecureStoragePlugin.get({
key: 'token_response',
});
if (!tokenResponse) {
console.log('No token found in secure storage.');
oktaAuth.signInWithRedirect(); // Redirect to login if no token is found
return;
}
// Parse the token
const parsedTokenResponse = JSON.parse(tokenResponse);
const accessToken = parsedTokenResponse?.access_token;
const refreshToken = parsedTokenResponse?.refresh_token;
if (!accessToken) {
console.log('Access token not found in secure storage.');
oktaAuth.signInWithRedirect(); // Redirect to login if no access token is found
return;
}
// Decode the token to check expiration
const tokenPayload = JSON.parse(atob(accessToken.split('.')[1])); // Decode the JWT payload
const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
if (tokenPayload.exp && tokenPayload.exp > currentTime) {
console.log(
'Token is valid based on expiration time. Storing in token manager.',
);
// Transform the token into the correct format
const tokenToStore = {
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: tokenPayload.exp,
scopes: ['openid', 'offline_access'],
pkce: true,
};
// Add the token to the token manager
oktaAuth.tokenManager.add('accessToken', tokenToStore);
} else if (refreshToken) {
console.log('Token expired. Attempting to refresh.');
try {
console.log('Refresh Token:', refreshToken); // Log the refresh token
const refreshedTokens = await oktaAuth.token.refresh(refreshToken);
oktaAuth.tokenManager.setTokens(refreshedTokens);
console.log('Token refreshed successfully.');
} catch (refreshError) {
console.error('Failed to refresh token:', refreshError);
console.log('Redirecting to login...');
oktaAuth.signInWithRedirect(); // Redirect to login if refresh fails
}
} else {
console.log('Token expired and no refresh token available.');
oktaAuth.signInWithRedirect(); // Redirect to login if no valid token is available
}
} catch (error) {
console.error('Error initializing authentication:', error);
oktaAuth.signInWithRedirect(); // Redirect to login if something goes wrong
}
};
const performTestCall = async () => {
try {
// Clear any saved auth transactions
oktaAuth.transactionManager.clear();
// Retrieve the access token from the token manager
let accessToken = await oktaAuth.tokenManager.get('accessToken');
if (!accessToken) {
console.log('No access token available. Redirecting to login.');
oktaAuth.signInWithRedirect();
return;
}
// Check if the token is expired
const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
if (accessToken.expiresAt && accessToken.expiresAt <= currentTime) {
console.log('Access token expired. Attempting to refresh.');
try {
oktaAuth.transactionManager.clear();
const refreshedTokens =
await oktaAuth.tokenManager.renew('accessToken');
accessToken = refreshedTokens.accessToken;
console.log('Token refreshed successfully.');
} catch (refreshError) {
console.error('Failed to refresh token:', refreshError);
console.log('Redirecting to login...');
oktaAuth.signInWithRedirect(); // Redirect to login if refresh fails
return;
}
}
// Make the API call with the valid access token
const route =
'https://xxx.co.uk/testCall';
const params = {
device: 'ion-ios/5.2.6',
};
const config = {
headers: {
Authorization: `Okta ${accessToken.accessToken}`,
'X-Requested-App': 'feed',
'X-Requested-App-User': '100000160',
'X-Requested-Type': 'ionic',
'Cache-Control': 'no-cache',
},
withCredentials: false,
};
console.log('Performing test call...');
const data = await fetcher(route, params, config);
console.log('Test call successful:', data);
alert(`Test call successful:\n${JSON.stringify(data, null, 2)}`);
} catch (error) {
console.error('Test call failed:', error);
alert(`Test call failed: ${error.message}`);
}
};
useEffect(() => {
const handleRedirectCallback = async () => {
try {
const tokens = await oktaAuth.token.parseFromUrl();
oktaAuth.tokenManager.setTokens(tokens);
} catch (error) {
console.error('Error handling redirect callback:', error);
}
};
const initializeApp = async () => {
await migrateTokens();
await initializeAuth();
};
console.log('App initialized. Checking for redirect callback...');
console.log('Current URL:', window.location.href);
if (window.location.pathname === '/callback') {
handleRedirectCallback();
} else {
initializeApp();
}
SplashScreen.hide();
}, []);
return (
<div style={styles.container}>
<button style={styles.button} onClick={performTestCall}>
Perform Test Call
</button>
</div>
);
};
const styles = {
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh', // Full viewport height
backgroundColor: '#f5f5f5', // Optional background color
},
button: {
padding: '10px 20px',
fontSize: '16px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
},
};
export default App;