From 874ad955aea0f7ff40c945aade141b606335b430 Mon Sep 17 00:00:00 2001 From: Sandros94 Date: Fri, 6 Sep 2024 17:08:36 +0200 Subject: [PATCH 1/3] feat(instagram): new provider --- playground/.env.example | 3 + playground/app.vue | 6 + playground/auth.d.ts | 1 + .../server/routes/auth/instagram.get.ts | 12 ++ src/module.ts | 6 + src/runtime/server/lib/oauth/instagram.ts | 137 ++++++++++++++++++ src/runtime/server/plugins/oauth.ts | 7 +- src/runtime/types/oauth-config.ts | 2 +- 8 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 playground/server/routes/auth/instagram.get.ts create mode 100644 src/runtime/server/lib/oauth/instagram.ts diff --git a/playground/.env.example b/playground/.env.example index 4670b016..7848c12b 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -41,6 +41,9 @@ NUXT_OAUTH_COGNITO_REGION= # Facebook NUXT_OAUTH_FACEBOOK_CLIENT_ID= NUXT_OAUTH_FACEBOOK_CLIENT_SECRET= +# Instagram +NUXT_OAUTH_INSTAGRAM_CLIENT_ID= +NUXT_OAUTH_INSTAGRAM_CLIENT_SECRET= # PayPal NUXT_OAUTH_PAYPAL_CLIENT_ID= NUXT_OAUTH_PAYPAL_CLIENT_SECRET= diff --git a/playground/app.vue b/playground/app.vue index 9be9358f..7d75538b 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -42,6 +42,12 @@ const providers = computed(() => disabled: Boolean(user.value?.facebook), icon: 'i-simple-icons-facebook', }, + { + label: session.value.user?.instagram || 'instagram', + to: '/auth/instagram', + disabled: Boolean(user.value?.instagram), + icon: 'i-simple-icons-instagram', + }, { label: session.value.user?.github || 'GitHub', to: '/auth/github', diff --git a/playground/auth.d.ts b/playground/auth.d.ts index 897c7ee8..8523d992 100644 --- a/playground/auth.d.ts +++ b/playground/auth.d.ts @@ -13,6 +13,7 @@ declare module '#auth-utils' { linkedin?: string cognito?: string facebook?: string + instagram?: string paypal?: string steam?: string x?: string diff --git a/playground/server/routes/auth/instagram.get.ts b/playground/server/routes/auth/instagram.get.ts new file mode 100644 index 00000000..750e1766 --- /dev/null +++ b/playground/server/routes/auth/instagram.get.ts @@ -0,0 +1,12 @@ +export default oauthInstagramEventHandler({ + async onSuccess(event, { user }) { + await setUserSession(event, { + user: { + instagram: user.username, + }, + loggedInAt: Date.now(), + }) + + return sendRedirect(event, '/') + }, +}) diff --git a/src/module.ts b/src/module.ts index ac244320..e9abd81f 100644 --- a/src/module.ts +++ b/src/module.ts @@ -160,6 +160,12 @@ export default defineNuxtModule({ clientSecret: '', redirectURL: '', }) + // Instagram OAuth + runtimeConfig.oauth.instagram = defu(runtimeConfig.oauth.instagram, { + clientId: '', + clientSecret: '', + redirectURL: '', + }) // PayPal OAuth runtimeConfig.oauth.paypal = defu(runtimeConfig.oauth.paypal, { clientId: '', diff --git a/src/runtime/server/lib/oauth/instagram.ts b/src/runtime/server/lib/oauth/instagram.ts new file mode 100644 index 00000000..6e2391eb --- /dev/null +++ b/src/runtime/server/lib/oauth/instagram.ts @@ -0,0 +1,137 @@ +import type { H3Event } from 'h3' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' +import { defu } from 'defu' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig, createError } from '#imports' +import type { OAuthConfig } from '#auth-utils' + +export interface OAuthInstagramConfig { + /** + * Instagram OAuth Client ID + * @default process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_ID + */ + clientId?: string + /** + * Instagram OAuth Client Secret + * @default process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_SECRET + */ + clientSecret?: string + /** + * Instagram OAuth Scope + * @default [ 'user_profile' ] + * @see https://developers.facebook.com/docs/instagram-basic-display-api/overview#permissions + * @example [ 'user_profile', 'user_media ], + */ + scope?: string[] + + /** + * Instagram OAuth User Fields + * @default [ 'id', 'username'], + * @see https://developers.facebook.com/docs/instagram-basic-display-api/reference/user#fields + * @example [ 'id', 'username', 'account_type', 'media_count' ], + */ + fields?: string[] + + /** + * Instagram OAuth Authorization URL + * @default 'https://api.instagram.com/oauth/authorize' + */ + authorizationURL?: string + + /** + * Instagram OAuth Token URL + * @default 'https://api.instagram.com/oauth/access_token' + */ + tokenURL?: string + + /** + * Extra authorization parameters to provide to the authorization URL + * @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow/ + */ + authorizationParams?: Record + /** + * Redirect URL to to allow overriding for situations like prod failing to determine public hostname + * @default process.env.NUXT_OAUTH_INSTAGRAM_REDIRECT_URL or current URL + */ + redirectURL?: string +} + +export function oauthInstagramEventHandler({ + config, + onSuccess, + onError, +}: OAuthConfig) { + return eventHandler(async (event: H3Event) => { + config = defu(config, useRuntimeConfig(event).oauth?.instagram, { + scoper: ['user_profile'], + authorizationURL: 'https://api.instagram.com/oauth/authorize', + tokenURL: 'https://api.instagram.com/oauth/access_token', + authorizationParams: {}, + }) as OAuthInstagramConfig + + const query = getQuery<{ code?: string, error?: string }>(event) + + if (query.error) { + const error = createError({ + statusCode: 401, + message: `Instagram login failed: ${query.error || 'Unknown error'}`, + data: query, + }) + if (!onError) throw error + return onError(event, error) + } + + if (!config.clientId) { + return handleMissingConfiguration(event, 'instagram', ['clientId'], onError) + } + + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { + config.scope = config.scope || [] + // Redirect to Instagram Oauth page + return sendRedirect( + event, + withQuery(config.authorizationURL as string, { + client_id: config.clientId, + redirect_uri: redirectURL, + scope: config.scope.join(' '), + }), + ) + } + + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + redirect_uri: redirectURL, + code: query.code, + }, + }) + + if (tokens.error) { + return handleAccessTokenErrorResponse(event, 'instagram', tokens, onError) + } + + const accessToken = tokens.access_token + // TODO: improve typing + + config.fields = config.fields || ['id', 'username'] + const fields = config.fields.join(',') + + const user = await $fetch( + `https://graph.instagram.com/v20.0/me?fields=${fields}&access_token=${accessToken}`, + ) + + if (!user) { + throw new Error('Instagram login failed: no user found') + } + + return onSuccess(event, { + user, + tokens, + }) + }) +} diff --git a/src/runtime/server/plugins/oauth.ts b/src/runtime/server/plugins/oauth.ts index 3f6cedf5..4a26b245 100644 --- a/src/runtime/server/plugins/oauth.ts +++ b/src/runtime/server/plugins/oauth.ts @@ -2,8 +2,11 @@ import type { NitroApp } from 'nitropack' import { defineNitroPlugin } from 'nitropack/runtime' export default defineNitroPlugin((nitroApp: NitroApp) => { - if (process.env.NUXT_OAUTH_FACEBOOK_CLIENT_ID && process.env.NUXT_OAUTH_FACEBOOK_CLIENT_SECRET) { - // In facebook login, the url is redirected to /#_=_ which is not a valid route + if ( + (process.env.NUXT_OAUTH_FACEBOOK_CLIENT_ID && process.env.NUXT_OAUTH_FACEBOOK_CLIENT_SECRET) + || (process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_ID && process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_SECRET) + ) { + // In facebook and instagram login, the url is redirected to /#_=_ which is not a valid route // so we remove it from the url, we are loading this long before the app is loaded // by using render:html hook // this is a hack, but it works diff --git a/src/runtime/types/oauth-config.ts b/src/runtime/types/oauth-config.ts index ce1bdd09..00b45c74 100644 --- a/src/runtime/types/oauth-config.ts +++ b/src/runtime/types/oauth-config.ts @@ -1,6 +1,6 @@ import type { H3Event, H3Error } from 'h3' -export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'facebook' | 'github' | 'google' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'x' | 'xsuaa' | 'yandex' | (string & {}) +export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'facebook' | 'github' | 'google' | 'instagram' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'x' | 'xsuaa' | 'yandex' | (string & {}) export type OnError = (event: H3Event, error: H3Error) => Promise | void From c97d87dae08bf8af2306bd6d422992f2180783fc Mon Sep 17 00:00:00 2001 From: Sandros94 Date: Fri, 6 Sep 2024 17:12:49 +0200 Subject: [PATCH 2/3] chore(instagram): add provider to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 40606c05..7160cb5b 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ It can also be set using environment variables: - Facebook - GitHub - Google +- Instagram - Keycloak - LinkedIn - Microsoft From eea9889459adf819081ddd5c1646b7a9b9c5a9e5 Mon Sep 17 00:00:00 2001 From: Sandros94 Date: Fri, 6 Sep 2024 18:17:04 +0200 Subject: [PATCH 3/3] fix(instagram): oauth query --- src/runtime/server/lib/oauth/instagram.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/runtime/server/lib/oauth/instagram.ts b/src/runtime/server/lib/oauth/instagram.ts index 6e2391eb..0a997128 100644 --- a/src/runtime/server/lib/oauth/instagram.ts +++ b/src/runtime/server/lib/oauth/instagram.ts @@ -21,7 +21,7 @@ export interface OAuthInstagramConfig { * Instagram OAuth Scope * @default [ 'user_profile' ] * @see https://developers.facebook.com/docs/instagram-basic-display-api/overview#permissions - * @example [ 'user_profile', 'user_media ], + * @example [ 'user_profile', 'user_media' ], */ scope?: string[] @@ -64,7 +64,7 @@ export function oauthInstagramEventHandler({ }: OAuthConfig) { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.instagram, { - scoper: ['user_profile'], + scope: ['user_profile'], authorizationURL: 'https://api.instagram.com/oauth/authorize', tokenURL: 'https://api.instagram.com/oauth/access_token', authorizationParams: {}, @@ -97,6 +97,7 @@ export function oauthInstagramEventHandler({ client_id: config.clientId, redirect_uri: redirectURL, scope: config.scope.join(' '), + response_type: 'code', }), ) }