Add window for manually copying the redirect URI for Nintendo Account authentication

This commit is contained in:
Samuel Elliott 2022-09-12 01:22:23 +01:00
parent ca0b063f58
commit ccdb4bac8d
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
4 changed files with 204 additions and 10 deletions

View File

@ -0,0 +1,139 @@
import React, { useCallback, useState } from 'react';
import { StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native';
import { Button } from '../components/index.js';
import { DEFAULT_ACCENT_COLOUR, HIGHLIGHT_COLOUR_DARK, HIGHLIGHT_COLOUR_LIGHT, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import ipc, { events } from '../ipc.js';
import { Root, useEventListener } from '../util.js';
export interface AddAccountManualPromptProps {
authoriseurl: string;
client_id: string;
}
export default function AddAccountManualPrompt(props: AddAccountManualPromptProps) {
const colour_scheme = useColorScheme();
const theme = colour_scheme === 'light' ? light : dark;
const [accent_colour, setAccentColour] = React.useState(() => ipc.getAccentColour());
useEventListener(events, 'systemPreferences:accent-colour', setAccentColour, []);
useEventListener(events, 'window:refresh', () => true, []);
const [callback_url, setCallbackUrl] = useState('');
const callback_url_valid = callback_url.startsWith('npf' + props.client_id + '://auth');
const save = useCallback(() => {
if (callback_url_valid) {
location.href = callback_url;
}
}, [callback_url, callback_url_valid]);
return <Root title="Add account" scrollable autoresize>
<View style={styles.main}>
<Text style={theme.text}>1. Login to your Nintendo Account</Text>
<Text style={[styles.help, theme.text]}>Do not select an account yet.</Text>
<View style={styles.buttonSingle}>
<Button title="Open Nintendo Account authorisation"
onPress={() => ipc.openExternalUrl(props.authoriseurl)}
color={'#' + (accent_colour ?? DEFAULT_ACCENT_COLOUR)} />
</View>
<Text style={[styles.header, theme.text]}>2. Enter the callback link</Text>
<Text style={[styles.help, theme.text]}>On the "Linking an External Account" page, right click "Select this person" and copy the link. It should start with "npf{props.client_id}://auth".</Text>
<Text style={[styles.help, theme.text]}>If you are adding a child account linked to your account, click "Select this person" next to their account to sign in as the child account, then with only the child account showing right click "Select this person" and copy the link.</Text>
<TextInput value={callback_url} onChangeText={setCallbackUrl}
placeholder={'npf' + props.client_id + '://auth#...'}
style={[styles.textInput, theme.textInput]} />
<View style={styles.buttons}>
<View style={styles.button}>
<Button title="Cancel"
onPress={() => window.close()}
color={'#' + (accent_colour ?? DEFAULT_ACCENT_COLOUR)} />
</View>
{callback_url_valid ? <View style={styles.button}>
<Button title="Add account"
onPress={save}
primary
color={'#' + (accent_colour ?? DEFAULT_ACCENT_COLOUR)} />
</View> : null}
</View>
</View>
</Root>;
}
const styles = StyleSheet.create({
loading: {
flex: 1,
paddingVertical: 50,
paddingHorizontal: 20,
justifyContent: 'center',
},
main: {
flex: 1,
paddingVertical: 20,
paddingHorizontal: 20,
},
header: {
marginTop: 12,
},
help: {
marginTop: 8,
fontSize: 13,
opacity: 0.7,
},
textInput: {
marginTop: 8,
paddingVertical: 5,
paddingHorizontal: 10,
borderRadius: 3,
fontSize: 13,
},
buttons: {
marginTop: 20,
flexDirection: 'row',
justifyContent: 'flex-end',
},
button: {
marginLeft: 10,
},
buttonSingle: {
marginTop: 10,
marginRight: 10,
flexDirection: 'row',
},
});
const light = StyleSheet.create({
text: {
color: TEXT_COLOUR_LIGHT,
},
picker: {
backgroundColor: HIGHLIGHT_COLOUR_LIGHT,
color: TEXT_COLOUR_LIGHT,
},
textInput: {
backgroundColor: HIGHLIGHT_COLOUR_LIGHT,
color: TEXT_COLOUR_LIGHT,
},
});
const dark = StyleSheet.create({
text: {
color: TEXT_COLOUR_DARK,
},
picker: {
backgroundColor: HIGHLIGHT_COLOUR_DARK,
color: TEXT_COLOUR_DARK,
},
textInput: {
backgroundColor: HIGHLIGHT_COLOUR_DARK,
color: TEXT_COLOUR_DARK,
},
});

View File

@ -5,12 +5,14 @@ import Friend from './friend/index.js';
import DiscordSetup from './discord/index.js';
import AddFriend from './add-friend/index.js';
import Preferences from './preferences/index.js';
import AddAccountManualPrompt from './add-account-manual/index.js';
AppRegistry.registerComponent('App', () => App);
AppRegistry.registerComponent('Friend', () => Friend);
AppRegistry.registerComponent('DiscordPresence', () => DiscordSetup);
AppRegistry.registerComponent('AddFriend', () => AddFriend);
AppRegistry.registerComponent('Preferences', () => Preferences);
AppRegistry.registerComponent('AddAccountManualPrompt', () => AddAccountManualPrompt);
const style = window.document.createElement('style');

View File

@ -6,6 +6,7 @@ export enum WindowType {
DISCORD_PRESENCE = 'DiscordPresence',
ADD_FRIEND = 'AddFriend',
PREFERENCES = 'Preferences',
ADD_ACCOUNT_MANUAL_PROMPT = 'AddAccountManualPrompt',
}
interface WindowProps {
@ -14,6 +15,7 @@ interface WindowProps {
[WindowType.DISCORD_PRESENCE]: import('../browser/discord/index.js').DiscordSetupProps;
[WindowType.ADD_FRIEND]: import('../browser/add-friend/index.js').AddFriendProps;
[WindowType.PREFERENCES]: import('../browser/preferences/index.js').PreferencesProps;
[WindowType.ADD_ACCOUNT_MANUAL_PROMPT]: import('../browser/add-account-manual/index.js').AddAccountManualPromptProps;
}
export interface WindowConfiguration<T extends WindowType = WindowType> {

View File

@ -11,6 +11,8 @@ import { getPctlToken, SavedMoonToken } from '../../common/auth/moon.js';
import { Jwt } from '../../util/jwt.js';
import { tryGetNativeImageFromUrl } from './util.js';
import { ZNCA_API_USE_URL } from '../../common/constants.js';
import { createWindow } from './windows.js';
import { WindowType } from '../common/types.js';
const debug = createDebug('app:main:na-auth');
@ -199,9 +201,16 @@ export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: stri
});
}
export function getSessionTokenCodeByDefaultBrowser(client_id: string, scope: string | string[], close_window = true) {
const FORCE_MANUAL_AUTH_URI_ENTRY = process.env.NXAPI_FORCE_MANUAL_AUTH === '1';
export function getSessionTokenCodeByDefaultBrowser(
client_id: string, scope: string | string[],
close_window = true,
force_manual = FORCE_MANUAL_AUTH_URI_ENTRY
) {
return new Promise<NintendoAccountSessionTokenCode>((rs, rj) => {
const {url: authoriseurl, state, verifier, challenge} = getAuthUrl(client_id, scope);
let window: BrowserWindow | undefined = undefined;
const handleAuthUrl = (url: URL) => {
const authorisedparams = new URLSearchParams(url.hash.substr(1));
@ -209,16 +218,19 @@ export function getSessionTokenCodeByDefaultBrowser(client_id: string, scope: st
if (authorisedparams.get('state') !== state) {
rj(new Error('Invalid state'));
window?.close();
return;
}
if (authorisedparams.has('error')) {
rj(AuthoriseError.fromSearchParams(authorisedparams));
window?.close();
return;
}
if (!authorisedparams.has('session_token_code')) {
rj(new Error('Response didn\'t include a session token code'));
window?.close();
return;
}
@ -227,7 +239,9 @@ export function getSessionTokenCodeByDefaultBrowser(client_id: string, scope: st
debug('code', code, jwt, sig);
rs({code, verifier});
if (window && close_window) window.close();
else if (window) rs({code, verifier, window});
else rs({code, verifier});
};
debug('Prompting user for Nintendo Account authorisation', {
@ -239,7 +253,10 @@ export function getSessionTokenCodeByDefaultBrowser(client_id: string, scope: st
const protocol = 'npf' + client_id;
if (app.isDefaultProtocolClient(protocol)) {
if (force_manual) {
debug('Manual entry forced, prompting for redirect URI');
window = askUserForRedirectUri(authoriseurl, client_id, handleAuthUrl, rj);
} else if (app.isDefaultProtocolClient(protocol)) {
debug('App is already default protocol handler, opening browser');
auth_state.set(state, [handleAuthUrl, rj, protocol]);
shell.openExternal(authoriseurl);
@ -247,12 +264,12 @@ export function getSessionTokenCodeByDefaultBrowser(client_id: string, scope: st
const registered_app = app.getApplicationNameForProtocol(protocol);
if (registered_app || !app.setAsDefaultProtocolClient(protocol)) {
debug('Another app is using the auth protocol or registration failed, prompting for redirect URI');
window = askUserForRedirectUri(authoriseurl, client_id, handleAuthUrl, rj);
} else {
debug('App is now default protocol handler, opening browser');
auth_state.set(state, [handleAuthUrl, rj, protocol]);
shell.openExternal(authoriseurl);
} else {
debug('Another app is using the auth protocol or registration failed, prompting for redirect URI');
askUserForRedirectUri(authoriseurl, handleAuthUrl, rj);
}
}
});
@ -285,8 +302,42 @@ app.on('quit', () => {
}
});
function askUserForRedirectUri(authoriseurl: string, rs: (url: URL) => void, rj: (reason: any) => void) {
rj(new Error('Not implemented'));
function askUserForRedirectUri(
authoriseurl: string, client_id: string,
handleAuthUrl: (url: URL) => void, rj: (reason: any) => void
) {
const window = createWindow(WindowType.ADD_ACCOUNT_MANUAL_PROMPT, {
authoriseurl,
client_id,
}, {
show: false,
maximizable: false,
minimizable: false,
width: 560,
height: 300,
minWidth: 450,
maxWidth: 700,
minHeight: 300,
maxHeight: 300,
});
window.webContents.on('will-navigate', (event, url_string) => {
event.preventDefault();
const url = new URL(url_string);
debug('will navigate', url);
if (url.protocol === 'npf' + client_id + ':' && url.host === 'auth') {
handleAuthUrl(url);
}
});
window.on('closed', () => {
rj(new AuthoriseCancelError('Canceled'));
});
return window;
}
const NSO_SCOPE = [
@ -300,7 +351,7 @@ const NSO_SCOPE = [
export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_browser = true) {
const {code, verifier, window} = use_in_app_browser ?
await getSessionTokenCodeByInAppBrowser(ZNCA_CLIENT_ID, NSO_SCOPE, false) :
await getSessionTokenCodeByDefaultBrowser(ZNCA_CLIENT_ID, NSO_SCOPE);
await getSessionTokenCodeByDefaultBrowser(ZNCA_CLIENT_ID, NSO_SCOPE, false);
window?.setFocusable(false);
window?.blurWebView();
@ -439,7 +490,7 @@ const MOON_SCOPE = [
export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_browser = true) {
const {code, verifier, window} = use_in_app_browser ?
await getSessionTokenCodeByInAppBrowser(ZNMA_CLIENT_ID, MOON_SCOPE, false) :
await getSessionTokenCodeByDefaultBrowser(ZNMA_CLIENT_ID, MOON_SCOPE);
await getSessionTokenCodeByDefaultBrowser(ZNMA_CLIENT_ID, MOON_SCOPE, false);
window?.setFocusable(false);
window?.blurWebView();