Skip to content

Commit e647040

Browse files
D3visionNLBekacru
andauthored
feat: add Roblox social provider (#1249)
* Implemented Roblox social provider + documentation + icon for builder * linter didn't like my svg and some of my other formatting * and now the linter is finally happy * made discord scope option overwrite default scopes, this way I dont need to have a users' email * Revert "made discord scope option overwrite default scopes, this way I dont need to have a users' email" This reverts commit 35b29e7. --------- Co-authored-by: Bereket Engida <[email protected]>
1 parent 047d677 commit e647040

File tree

4 files changed

+158
-0
lines changed

4 files changed

+158
-0
lines changed

docs/components/builder/social-provider.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,23 @@ export const socialProviders = {
321321
),
322322
stringIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 448 512"><path fill="currentColor" d="M64 32C28.7 32 0 60.7 0 96v320c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm297.1 84L257.3 234.6L379.4 396h-95.6L209 298.1L123.3 396H75.8l111-126.9L69.7 116h98l67.7 89.5l78.2-89.5zm-37.8 251.6L153.4 142.9h-28.3l171.8 224.7h26.3z"></path></svg>`,
323323
},
324+
roblox: {
325+
Icon: (props?: SVGProps<any>) => (
326+
<svg
327+
xmlns="http://www.w3.org/2000/svg"
328+
width="1.2em"
329+
height="1.2em"
330+
viewBox="0 0 267 267"
331+
{...props}
332+
>
333+
<path
334+
fill="currentColor"
335+
d="M 56.926 0.986 L 1.01 210.05 L 210.073 266.014 L 265.989 56.951 L 56.926 0.986 Z M 112.15 96.988 L 170.481 112.619 L 154.849 170.95 L 96.47 155.318 L 112.15 96.988 Z"
336+
></path>
337+
</svg>
338+
),
339+
stringIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 267 267"><path xmlns="http://www.w3.org/2000/svg" fill="currentColor" d="M 56.926 0.986 L 1.01 210.05 L 210.073 266.014 L 265.989 56.951 L 56.926 0.986 Z M 112.15 96.988 L 170.481 112.619 L 154.849 170.95 L 96.47 155.318 L 112.15 96.988 Z"/></svg>`,
340+
},
324341
vk: {
325342
Icon: (props?: SVGProps<any>) => (
326343
<svg
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: Roblox
3+
description: Roblox provider setup and usage.
4+
---
5+
6+
<Steps>
7+
<Step>
8+
### Get your Roblox Credentials
9+
Get your Roblox credentials from the [Roblox Creator Hub](https://create.roblox.com/dashboard/credentials?activeTab=OAuthTab).
10+
11+
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/roblox` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
12+
13+
<Callout type="info">
14+
The Roblox API does not provide email addresses. As a workaround, the user's `email` field uses the `preferred_username` value instead.
15+
</Callout>
16+
</Step>
17+
18+
<Step>
19+
### Configure the provider
20+
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
21+
22+
```ts title="auth.ts"
23+
import { betterAuth } from "better-auth"
24+
25+
export const auth = betterAuth({
26+
socialProviders: {
27+
roblox: { // [!code highlight]
28+
clientId: process.env.ROBLOX_CLIENT_ID, // [!code highlight]
29+
clientSecret: process.env.ROBLOX_CLIENT_SECRET, // [!code highlight]
30+
}, // [!code highlight]
31+
},
32+
})
33+
```
34+
</Step>
35+
<Step>
36+
### Sign In with Roblox
37+
To sign in with Roblox, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
38+
- `provider`: The provider to use. It should be set to `roblox`.
39+
40+
```ts title="auth-client.ts"
41+
import { createAuthClient } from "better-auth/client"
42+
const authClient = createAuthClient()
43+
44+
const signIn = async () => {
45+
const data = await authClient.signIn.social({
46+
provider: "roblox"
47+
})
48+
}
49+
```
50+
</Step>
51+
</Steps>

packages/better-auth/src/social-providers/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { dropbox } from "./dropbox";
1212
import { linkedin } from "./linkedin";
1313
import { gitlab } from "./gitlab";
1414
import { reddit } from "./reddit";
15+
import { roblox } from "./roblox";
1516
import { z } from "zod";
1617
import { vk } from "./vk";
1718
export const socialProviders = {
@@ -28,6 +29,7 @@ export const socialProviders = {
2829
linkedin,
2930
gitlab,
3031
reddit,
32+
roblox,
3133
vk,
3234
};
3335

@@ -65,4 +67,5 @@ export * from "./dropbox";
6567
export * from "./linkedin";
6668
export * from "./gitlab";
6769
export * from "./reddit";
70+
export * from "./roblox";
6871
export * from "./vk";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { betterFetch } from "@better-fetch/fetch";
2+
import type { OAuthProvider, ProviderOptions } from "../oauth2";
3+
import { validateAuthorizationCode } from "../oauth2";
4+
5+
export interface RobloxProfile extends Record<string, any> {
6+
/** the user's id */
7+
sub: string;
8+
/** the user's username */
9+
preferred_username: string;
10+
/** the user's display name, will return the same value as the preffered_username if not set */
11+
nickname: string;
12+
/** the user's display name, again, will return the same value as the preffered_username if not set */
13+
name: string;
14+
/** the account creation date as a unix timestamp in seconds */
15+
created_at: number;
16+
/** the user's profile url */
17+
profile: string;
18+
/** the user's avatar url */
19+
picture: string;
20+
}
21+
22+
export interface RobloxOptions extends ProviderOptions<RobloxProfile> {
23+
prompt?:
24+
| "none"
25+
| "consent"
26+
| "login"
27+
| "select_account"
28+
| "select_account+consent";
29+
}
30+
31+
export const roblox = (options: RobloxOptions) => {
32+
return {
33+
id: "roblox",
34+
name: "Roblox",
35+
createAuthorizationURL({ state, scopes, redirectURI }) {
36+
const _scopes = scopes || ["openid", "profile"];
37+
options.scope && _scopes.push(...options.scope);
38+
return new URL(
39+
`https://apis.roblox.com/oauth/v1/authorize?scope=${_scopes.join(
40+
"+",
41+
)}&response_type=code&client_id=${
42+
options.clientId
43+
}&redirect_uri=${encodeURIComponent(
44+
options.redirectURI || redirectURI,
45+
)}&state=${state}&prompt=${options.prompt || "select_account+consent"}`,
46+
);
47+
},
48+
validateAuthorizationCode: async ({ code, redirectURI }) => {
49+
return validateAuthorizationCode({
50+
code,
51+
redirectURI: options.redirectURI || redirectURI,
52+
options,
53+
tokenEndpoint: "https://apis.roblox.com/oauth/v1/token",
54+
authentication: "post",
55+
});
56+
},
57+
async getUserInfo(token) {
58+
if (options.getUserInfo) {
59+
return options.getUserInfo(token);
60+
}
61+
const { data: profile, error } = await betterFetch<RobloxProfile>(
62+
"https://apis.roblox.com/oauth/v1/userinfo",
63+
{
64+
headers: {
65+
authorization: `Bearer ${token.accessToken}`,
66+
},
67+
},
68+
);
69+
70+
if (error) {
71+
return null;
72+
}
73+
return {
74+
user: {
75+
id: profile.sub,
76+
name: profile.nickname || profile.preferred_username || "",
77+
image: profile.picture,
78+
email: profile.preferred_username || null, // Roblox does not provide email
79+
emailVerified: true,
80+
},
81+
data: {
82+
...profile,
83+
},
84+
};
85+
},
86+
} satisfies OAuthProvider<RobloxProfile>;
87+
};

0 commit comments

Comments
 (0)