diff --git a/locales/en-US.yml b/locales/en-US.yml
index a6a8a2110..10e0e4c65 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -828,6 +828,8 @@ setTag: "Set tag"
addTag: "Add tag"
removeTag: "Remove tag"
externalCssSnippets: "Some CSS snippets for your inspiration (not managed by FoundKey)"
+oauthErrorGoBack: "An error happened while trying to authenticate a 3rd party app.\
+ \ Please go back and try again."
_emailUnavailable:
used: "This email address is already being used"
format: "The format of this email address is invalid"
diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
index eeb51abc6..e1b498a49 100644
--- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts
+++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
@@ -23,6 +23,19 @@ export const meta = {
optional: false, nullable: false,
format: 'url',
},
+ // stuff that auth/session/show would respond with
+ id: {
+ type: 'string',
+ description: 'The ID of the authentication session. Same as returned by `auth/session/show`.',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ app: {
+ type: 'object',
+ description: 'The App requesting permissions. Same as returned by `auth/session/show`.',
+ optional: false, nullable: false,
+ ref: 'App',
+ },
},
},
@@ -30,17 +43,27 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- appSecret: { type: 'string' },
- },
- required: ['appSecret'],
+ oneOf: [{
+ type: 'object',
+ properties: {
+ clientId: { type: 'string' },
+ },
+ required: ['clientId']
+ }, {
+ type: 'object',
+ properties: {
+ appSecret: { type: 'string' },
+ },
+ required: ['appSecret'],
+ }],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
// Lookup app
- const app = await Apps.findOneBy({
+ const app = await Apps.findOneBy(ps.clientId ? {
+ id: ps.clientId,
+ } : {
secret: ps.appSecret,
});
@@ -50,10 +73,11 @@ export default define(meta, paramDef, async (ps) => {
// Generate token
const token = uuid();
+ const id = genId();
// Create session token document
const doc = await AuthSessions.insert({
- id: genId(),
+ id,
createdAt: new Date(),
appId: app.id,
token,
@@ -62,5 +86,7 @@ export default define(meta, paramDef, async (ps) => {
return {
token: doc.token,
url: `${config.authUrl}/${doc.token}`,
+ id,
+ app: await Apps.pack(app),
};
});
diff --git a/packages/client/src/pages/auth.vue b/packages/client/src/pages/auth.vue
index 3b3ff7bac..ac9a3e051 100644
--- a/packages/client/src/pages/auth.vue
+++ b/packages/client/src/pages/auth.vue
@@ -6,7 +6,7 @@
ref="form"
class="form"
:session="session"
- @denied="state = 'denied'"
+ @denied="denied"
@accepted="accepted"
/>
@@ -20,6 +20,9 @@
{{ i18n.ts.somethingHappened }}
+
+
{{ i18n.ts.oauthErrorGoBack }}
+
@@ -37,43 +40,115 @@ import { i18n } from '@/i18n';
import { query, appendQuery } from '@/scripts/url';
const props = defineProps<{
- token: string;
+ token?: string;
}>();
-let state: 'fetching' | 'waiting' | 'denied' | 'accepted' | 'fetch-session-error' = $ref('fetching');
+let state: 'fetching' | 'waiting' | 'denied' | 'accepted' | 'fetch-session-error' | 'oauth-error' = $ref('fetching');
let session = $ref(null);
-onMounted(() => {
+// if this is an OAuth request, will contain the respective parameters
+let oauth: { state: string | null, callback: string } | null = null;
+
+onMounted(async () => {
if (!$i) return;
- // Fetch session
- os.api('auth/session/show', {
- token: props.token,
- }).then(fetchedSession => {
- session = fetchedSession;
+ // detect whether this is actual OAuth or "legacy" auth
+ const params = new URLSearchParams(location.search);
+ if (params.get('response_type') === 'code') {
+ // OAuth request detected!
- // 既に連携していた場合
- if (session.app.isAuthorized) {
- os.api('auth/accept', {
- token: session.token,
- }).then(() => {
- this.accepted();
- });
- } else {
- state = 'waiting';
+ // as a kind of hack, we first have to start the session for the OAuth client
+ const clientId = params.get('client_id');
+ if (!clientId) {
+ state = 'fetch-session-error';
+ return;
}
- }).catch(() => {
+
+ session = await os.api('auth/session/generate', {
+ clientId,
+ }).catch(e => {
+ const response = {
+ error: 'server_error',
+ ...(oauth.state ? { state: oauth.state } : {}),
+ };
+ // try to determine the cause of the error
+ if (e.code === 'NO_SUCH_APP') {
+ response.error = 'invalid_request';
+ response.error_description = 'unknown client_id';
+ } else if (e.message) {
+ response.error_description = e.message;
+ }
+
+ if (params.has('redirect_uri')) {
+ location.href = appendQuery(params.get('redirect_uri'), query(response));
+ } else {
+ state = 'oauth-error';
+ }
+ });
+
+ oauth = {
+ state: params.get('state'),
+ callback: params.get('redirect_uri') ?? session.app.callbackUrl,
+ };
+ } else if (!props.token) {
state = 'fetch-session-error';
- });
+ } else {
+ session = await os.api('auth/session/show', {
+ token: props.token,
+ }).catch(() => {
+ state = 'fetch-session-error';
+ });
+ }
+
+ // abort if an error occurred
+ if (['fetch-session-error', 'oauth-error'].includes(state)) return;
+
+ // check whether the user already authorized the app earlier
+ if (session.app.isAuthorized) {
+ // already authorized, move on through!
+ os.api('auth/accept', {
+ token: session.token,
+ }).then(() => {
+ accepted();
+ });
+ } else {
+ // user still has to give consent
+ state = 'waiting';
+ }
});
function accepted(): void {
state = 'accepted';
- if (session.app.callbackUrl) {
+ if (oauth) {
+ // redirect with authorization token
+ const params = {
+ code: session.token,
+ ...(oauth.state ? { state: oauth.state } : {}),
+ };
+
+ location.href = appendQuery(oauth.callback, query(params));
+ } else if (session.app.callbackUrl) {
+ // do whatever the legacy auth did
location.href = appendQuery(session.app.callbackUrl, query({ token: session.token }));
}
}
+function denied(): void {
+ state = 'denied';
+ if (oauth) {
+ // redirect with error code
+ const params = {
+ error: 'access_denied',
+ error_description: 'The user denied permission.',
+ ...(oauth.state ? { state: oauth.state } : {}),
+ };
+
+ location.href = appendQuery(oauth.callback, query(params));
+ } else {
+ // legacy auth didn't do anything in this case...
+ }
+}
+
function onLogin(res): void {
login(res.i);
}
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index 6d3011688..791076e71 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -94,6 +94,9 @@ export const routes = [{
}, {
path: '/preview',
component: page(() => import('./pages/preview.vue')),
+}, {
+ path: '/auth',
+ component: page(() => import('./pages/auth.vue')),
}, {
path: '/auth/:token',
component: page(() => import('./pages/auth.vue')),