Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support biometric in Capacitor SDK #257

Merged
merged 15 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions example/capacitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Ionic Capacitor Example App

Example app of ionic app using capacitor.

## Project config

We need the following redirect uris to work:

```
http://localhost:8100/reauth-redirect
http://localhost:8100/oauth-redirect
com.authgear.exampleapp.capacitor://host/path
https://localhost
capacitor://localhost
```

## Run the app

Before running the app, build the latest sdk at project root.

```sh
npm i
npm run build
```

Then, in this directory, run the exmaple app.

```sh
cd example/capacitor
npm i
# Run it in ios device
npm run run-ios
# OR, run it in android device
npm run run-android
```
2 changes: 2 additions & 0 deletions example/capacitor/ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to authenticate</string>
</dict>
</plist>
10 changes: 5 additions & 5 deletions example/capacitor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion example/capacitor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"build": "tsc && vite build",
"preview": "vite preview",
"test.unit": "vitest",
"lint": "eslint"
"lint": "eslint",
"run-ios": "cap run ios",
"run-android": "cap run android"
},
"dependencies": {
"@authgear/capacitor": "../../packages/authgear-capacitor",
Expand Down
193 changes: 189 additions & 4 deletions example/capacitor/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ import authgearCapacitor, {
CancelError as CapacitorCancelError,
ColorScheme,
Page as CapacitorPage,
BiometricOptions,
BiometricAccessConstraintIOS,
BiometricLAPolicy,
BiometricAccessConstraintAndroid,
} from "@authgear/capacitor";
import {
readClientID,
Expand All @@ -59,6 +63,22 @@ const REDIRECT_URI_WEB_AUTHENTICATE = "http://localhost:8100/oauth-redirect";
const REDIRECT_URI_WEB_REAUTH = "http://localhost:8100/reauth-redirect";
const REDIRECT_URI_CAPACITOR = "com.authgear.exampleapp.capacitor://host/path";

const biometricOptions: BiometricOptions = {
ios: {
localizedReason: "Use biometric to authenticate",
constraint: BiometricAccessConstraintIOS.BiometryCurrentSet,
policy: BiometricLAPolicy.deviceOwnerAuthenticationWithBiometrics,
},
android: {
title: "Biometric Authentication",
subtitle: "Biometric authentication",
description: "Use biometric to authenticate",
negativeButtonText: "Cancel",
constraint: [BiometricAccessConstraintAndroid.BiometricStrong],
invalidatedByBiometricEnrollment: true,
},
};

function isPlatformWeb(): boolean {
return Capacitor.getPlatform() === "web";
}
Expand All @@ -83,6 +103,7 @@ function AuthgearDemo() {
const [isSSOEnabled, setIsSSOEnabled] = useState(() => {
return readIsSSOEnabled();
});
const [biometricEnabled, setBiometricEnabled] = useState<boolean>(false);

const [sessionState, setSessionState] = useState<SessionState | null>(() => {
if (isPlatformWeb()) {
Expand All @@ -105,6 +126,20 @@ function AuthgearDemo() {
return d;
}, [setSessionState]);

const updateBiometricState = useCallback(async () => {
if (isPlatformWeb()) {
return;
}

try {
await authgearCapacitor.checkBiometricSupported(biometricOptions);
const enabled = await authgearCapacitor.isBiometricEnabled();
setBiometricEnabled(enabled);
} catch (e) {
console.error(e);
}
}, []);

const showError = useCallback((e: any) => {
const json = JSON.parse(JSON.stringify(e));
json["constructor.name"] = e?.constructor?.name;
Expand All @@ -129,6 +164,7 @@ function AuthgearDemo() {
}, []);

const postConfigure = useCallback(async () => {
await updateBiometricState();
const sessionState = isPlatformWeb()
? authgearWeb.sessionState
: authgearCapacitor.sessionState;
Expand All @@ -144,7 +180,7 @@ function AuthgearDemo() {
}

setInitialized(true);
}, []);
}, [updateBiometricState]);

const configure = useCallback(async () => {
setLoading(true);
Expand Down Expand Up @@ -207,8 +243,48 @@ function AuthgearDemo() {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [colorScheme, page, showError, showUserInfo, updateBiometricState]);

const enableBiometric = useCallback(async () => {
setLoading(true);
try {
await authgearCapacitor.enableBiometric(biometricOptions);
} catch (e: unknown) {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [showError, updateBiometricState]);

const authenticateBiometric = useCallback(async () => {
setLoading(true);
try {
const { userInfo } = await authgearCapacitor.authenticateBiometric(
biometricOptions
);
showUserInfo(userInfo);
} catch (e: unknown) {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [showError, showUserInfo, updateBiometricState]);

const disableBiometric = useCallback(async () => {
setLoading(true);
try {
await authgearCapacitor.disableBiometric();
} catch (e: unknown) {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [colorScheme, page, showError, showUserInfo]);
}, [showError, updateBiometricState]);

const showAuthTime = useCallback(() => {
if (isPlatformWeb()) {
Expand All @@ -224,7 +300,7 @@ function AuthgearDemo() {
}
}, []);

const reauthenticate = useCallback(async () => {
const reauthenticateWebOnly = useCallback(async () => {
setLoading(true);
try {
if (isPlatformWeb()) {
Expand Down Expand Up @@ -260,6 +336,45 @@ function AuthgearDemo() {
}
}, [showError, colorScheme, showAuthTime]);

const reauthenticate = useCallback(async () => {
setLoading(true);
try {
if (isPlatformWeb()) {
await authgearWeb.refreshIDToken();
if (!authgearWeb.canReauthenticate()) {
throw new Error(
"canReauthenticate() returns false for the current user"
);
}

authgearWeb.startReauthentication({
redirectURI: REDIRECT_URI_WEB_REAUTH,
});
} else {
await authgearCapacitor.refreshIDToken();
if (!authgearCapacitor.canReauthenticate()) {
throw new Error(
"canReauthenticate() returns false for the current user"
);
}

await authgearCapacitor.reauthenticate(
{
redirectURI: REDIRECT_URI_CAPACITOR,
colorScheme:
colorScheme === "" ? undefined : (colorScheme as ColorScheme),
},
biometricOptions
);
showAuthTime();
}
} catch (e) {
showError(e);
} finally {
setLoading(false);
}
}, [showError, colorScheme, showAuthTime]);

const openSettings = useCallback(async () => {
if (isPlatformWeb()) {
authgearWeb.open(WebPage.Settings);
Expand Down Expand Up @@ -409,6 +524,46 @@ function AuthgearDemo() {
[reauthenticate]
);

const onClickReauthenticateWebOnly = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

reauthenticateWebOnly();
},
[reauthenticateWebOnly]
);

const onClickEnableBiometric = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

enableBiometric();
},
[enableBiometric]
);

const onClickAuthenticateBiometric = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

authenticateBiometric();
},
[authenticateBiometric]
);

const onClickDisableBiometric = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

disableBiometric();
},
[disableBiometric]
);

const onClickOpenSettings = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
Expand Down Expand Up @@ -525,10 +680,40 @@ function AuthgearDemo() {
<IonButton
className="button"
disabled={!initialized || loading || !loggedIn}
onClick={onClickReauthenticate}
onClick={onClickReauthenticateWebOnly}
>
Re-authenticate
</IonButton>
<IonButton
className="button"
disabled={!initialized || loading || !loggedIn}
onClick={onClickReauthenticate}
>
Re-authenticate (biometric or web)
</IonButton>
{isPlatformWeb() ? null : (
<IonButton
className="button"
disabled={!initialized || loading || !loggedIn || biometricEnabled}
onClick={onClickEnableBiometric}
>
Enable biometric
</IonButton>
)}
<IonButton
className="button"
disabled={!initialized || loading || !biometricEnabled}
onClick={onClickDisableBiometric}
>
Disable biometric
</IonButton>
<IonButton
className="button"
disabled={!initialized || loading || loggedIn || !biometricEnabled}
onClick={onClickAuthenticateBiometric}
>
Authenticate with biometric
</IonButton>
<IonButton
className="button"
disabled={!initialized || !loggedIn}
Expand Down
Loading