From ba16989e30691e68431230e6f2bd12e84c57bdcd Mon Sep 17 00:00:00 2001 From: pilcrow Date: Thu, 28 Mar 2024 14:08:55 +0900 Subject: [PATCH] Add Roblox provider (#88) * add roblox provider * format --- .changesets/94a8i.minor.md | 1 + .changesets/hu3zn.minor.md | 2 +- .github/FUNDING.yml | 2 +- CONTRIBUTING.md | 2 +- docs/malta.config.json | 1 + docs/pages/providers/roblox.md | 46 ++++++++++++++++++++ docs/pages/providers/yandex.md | 2 +- src/index.ts | 2 + src/providers/roblox.ts | 78 ++++++++++++++++++++++++++++++++++ src/providers/yandex.ts | 12 ++++-- 10 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 .changesets/94a8i.minor.md create mode 100644 docs/pages/providers/roblox.md create mode 100644 src/providers/roblox.ts diff --git a/.changesets/94a8i.minor.md b/.changesets/94a8i.minor.md new file mode 100644 index 00000000..690e9359 --- /dev/null +++ b/.changesets/94a8i.minor.md @@ -0,0 +1 @@ +Add Roblox provider. diff --git a/.changesets/hu3zn.minor.md b/.changesets/hu3zn.minor.md index 70aaebc1..3910082a 100644 --- a/.changesets/hu3zn.minor.md +++ b/.changesets/hu3zn.minor.md @@ -1 +1 @@ -Add VK provider. \ No newline at end of file +Add VK provider. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 384b3910..dc31761a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: pilcrowOnPaper \ No newline at end of file +github: pilcrowOnPaper diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f16df734..ce2dc6d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ We welcome all contributions to the docs, especially grammar fixes. Arctic uses ## Contributing to the source code -We are open to most contributions, but please open a new issue before creating a pull request, especially for new features. It's likely your PR will be rejected if not. We have intentionally limited the scope of the project and we would like to keep the package lean. +We are open to most contributions, but please open a new issue before creating a pull request, especially for new features. It's likely your PR will be rejected if not. We have intentionally limited the scope of the project and we would like to keep the package lean. ### Set up diff --git a/docs/malta.config.json b/docs/malta.config.json index 3ee71e5b..d977dcd3 100644 --- a/docs/malta.config.json +++ b/docs/malta.config.json @@ -42,6 +42,7 @@ ["osu!", "/providers/osu"], ["Patreon", "/providers/patreon"], ["Reddit", "/providers/reddit"], + ["Roblox", "/providers/roblox"], ["Salesforce", "/providers/salesforce"], ["Slack", "/providers/slack"], ["Spotify", "/providers/spotify"], diff --git a/docs/pages/providers/roblox.md b/docs/pages/providers/roblox.md new file mode 100644 index 00000000..5cc668f2 --- /dev/null +++ b/docs/pages/providers/roblox.md @@ -0,0 +1,46 @@ +--- +title: "Roblox" +--- + +# Roblox + +Implements OpenID Connect. For confidential clients and not public clients. + +For usage, see [OAuth 2.0 provider with PKCE](/guides/oauth2-pkce). + +```ts +import { Roblox } from "arctic"; + +const roblox = new Roblox(clientId, clientSecret, redirectURI); +``` + +```ts +const url: URL = await roblox.createAuthorizationURL(state, codeVerifier, { + // optional + scopes // "openid" always included +}); +const tokens: RobloxTokens = await roblox.validateAuthorizationCode(code, codeVerifier); +const tokens: RobloxTokens = await roblox.refreshAccessToken(refreshToken); +``` + +## Get user profile + +Add the `profile` scope. + +```ts +const url = await roblox.createAuthorizationURL(state, codeVerifier, { + scopes: ["profile"] +}); +``` + +Parse the ID token or use the [`userinfo` endpoint](https://create.roblox.com/docs/cloud/reference/oauth2#get-v1userinfo). + +```ts +const tokens = await roblox.validateAuthorizationCode(code, codeVerifier); +const response = await fetch("https://apis.roblox.com/oauth/v1/userinfo", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } +}); +const user = await response.json(); +``` diff --git a/docs/pages/providers/yandex.md b/docs/pages/providers/yandex.md index 940dc793..35274af0 100644 --- a/docs/pages/providers/yandex.md +++ b/docs/pages/providers/yandex.md @@ -36,4 +36,4 @@ const response = await fetch("https://api.tracker.yandex.net/v2/myself", { } }); const user = await response.json(); -``` \ No newline at end of file +``` diff --git a/src/index.ts b/src/index.ts index 4c747799..0cf1d6f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export { Okta } from "./providers/okta.js"; export { Osu } from "./providers/osu.js"; export { Patreon } from "./providers/patreon.js"; export { Reddit } from "./providers/reddit.js"; +export { Roblox } from "./providers/roblox.js"; export { Salesforce } from "./providers/salesforce.js"; export { Slack } from "./providers/slack.js"; export { Spotify } from "./providers/spotify.js"; @@ -68,6 +69,7 @@ export type { OktaTokens } from "./providers/okta.js"; export type { OsuTokens } from "./providers/osu.js"; export type { PatreonTokens } from "./providers/patreon.js"; export type { RedditTokens } from "./providers/reddit.js"; +export type { RobloxTokens } from "./providers/roblox.js"; export type { SalesforceTokens } from "./providers/salesforce.js"; export type { SlackTokens } from "./providers/slack.js"; export type { SpotifyTokens } from "./providers/spotify.js"; diff --git a/src/providers/roblox.ts b/src/providers/roblox.ts new file mode 100644 index 00000000..f83661aa --- /dev/null +++ b/src/providers/roblox.ts @@ -0,0 +1,78 @@ +import { TimeSpan, createDate } from "oslo"; +import { OAuth2Client } from "oslo/oauth2"; + +import type { OAuth2ProviderWithPKCE } from "../index.js"; + +const authorizeEndpoint = "https://apis.roblox.com/oauth/v1/authorize"; +const tokenEndpoint = "https://apis.roblox.com/oauth/v1/token"; + +export class Roblox implements OAuth2ProviderWithPKCE { + private client: OAuth2Client; + private clientSecret: string; + + constructor(clientId: string, clientSecret: string, redirectURI: string) { + this.client = new OAuth2Client(clientId, authorizeEndpoint, tokenEndpoint, { + redirectURI + }); + this.clientSecret = clientSecret; + } + + public async createAuthorizationURL( + state: string, + codeVerifier: string, + options?: { + scopes?: string[]; + } + ): Promise { + const scopes = options?.scopes ?? []; + return await this.client.createAuthorizationURL({ + state, + codeVerifier, + scopes: [...scopes, "openid"] + }); + } + + public async validateAuthorizationCode( + code: string, + codeVerifier: string + ): Promise { + const result = await this.client.validateAuthorizationCode(code, { + credentials: this.clientSecret, + codeVerifier + }); + const tokens: RobloxTokens = { + accessToken: result.access_token, + refreshToken: result.refresh_token, + accessTokenExpiresAt: createDate(new TimeSpan(result.expires_in, "s")), + idToken: result.id_token + }; + return tokens; + } + + public async refreshAccessToken(refreshToken: string): Promise { + const result = await this.client.refreshAccessToken(refreshToken, { + credentials: this.clientSecret + }); + const tokens: RobloxTokens = { + accessToken: result.access_token, + refreshToken: result.refresh_token, + accessTokenExpiresAt: createDate(new TimeSpan(result.expires_in, "s")), + idToken: result.id_token + }; + return tokens; + } +} + +interface TokenResponseBody { + access_token: string; + refresh_token: string; + expires_in: number; + id_token: string; +} + +export interface RobloxTokens { + accessToken: string; + refreshToken: string; + accessTokenExpiresAt: Date; + idToken: string; +} diff --git a/src/providers/yandex.ts b/src/providers/yandex.ts index e38d123e..bb7a1e56 100644 --- a/src/providers/yandex.ts +++ b/src/providers/yandex.ts @@ -10,9 +10,13 @@ export class Yandex implements OAuth2Provider { private client: OAuth2Client; private clientSecret: string; - constructor(clientId: string, clientSecret: string, options?: { - redirectURI: string - }) { + constructor( + clientId: string, + clientSecret: string, + options?: { + redirectURI: string; + } + ) { this.client = new OAuth2Client(clientId, authorizeEndpoint, tokenEndpoint, { redirectURI: options?.redirectURI }); @@ -27,7 +31,7 @@ export class Yandex implements OAuth2Provider { ): Promise { return await this.client.createAuthorizationURL({ state, - scopes: options?.scopes ?? [], + scopes: options?.scopes ?? [] }); }