Skip to content

Commit

Permalink
feat: add instagram provider
Browse files Browse the repository at this point in the history
* feat(instagram): new provider

* chore(instagram): add provider to readme

* fix(instagram): oauth query

---------

Co-authored-by: Sébastien Chopin <seb@nuxt.com>
  • Loading branch information
sandros94 and atinux authored Sep 11, 2024
1 parent ddf38c1 commit 3bd553c
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ It can also be set using environment variables:
- GitHub
- GitLab
- Google
- Instagram
- Keycloak
- LinkedIn
- Microsoft
Expand Down
3 changes: 3 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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=
Expand Down
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare module '#auth-utils' {
linkedin?: string
cognito?: string
facebook?: string
instagram?: string
paypal?: string
steam?: string
x?: string
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/instagram.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default oauthInstagramEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
instagram: user.username,
},
loggedInAt: Date.now(),
})

return sendRedirect(event, '/')
},
})
6 changes: 6 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ export default defineNuxtModule<ModuleOptions>({
clientSecret: '',
redirectURL: '',
})
// Instagram OAuth
runtimeConfig.oauth.instagram = defu(runtimeConfig.oauth.instagram, {
clientId: '',
clientSecret: '',
redirectURL: '',
})
// PayPal OAuth
runtimeConfig.oauth.paypal = defu(runtimeConfig.oauth.paypal, {
clientId: '',
Expand Down
138 changes: 138 additions & 0 deletions src/runtime/server/lib/oauth/instagram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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<string, string>
/**
* 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<OAuthInstagramConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.instagram, {
scope: ['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(' '),
response_type: 'code',
}),
)
}

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,
})
})
}
7 changes: 5 additions & 2 deletions src/runtime/server/plugins/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/types/oauth-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { H3Event, H3Error } from 'h3'

export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'facebook' | 'github' | 'gitlab' | 'google' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'x' | 'xsuaa' | 'yandex' | (string & {})
export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'x' | 'xsuaa' | 'yandex' | (string & {})

export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void

Expand Down

0 comments on commit 3bd553c

Please sign in to comment.