Skip to content

Commit

Permalink
📝 Document oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
coyotte508 committed Jan 9, 2024
1 parent 655403a commit dbf0327
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 148 deletions.
20 changes: 20 additions & 0 deletions packages/hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ for await (const fileInfo of listFiles({repo})) {
await deleteRepo({ repo, credentials });
```

## OAuth Login

It's possible to login using OAuth (["Sign in with HF"](https://huggingface.co/docs/hub/oauth)).

This will allow you get an access token to use some of the API, depending of the scopes set inside the Space or the OAuth App.

```ts
import { oauthLogin, oauthHandleRedirectIfPresent } from "@huggingface/hub";

const oauthResult = await oauthHandleRedirectIfPresent();

if (!oauthResult) {
// If the user is not logged in, redirect to the login page
oauthLogin();
}

// You can use oauthResult.accessToken and oauthResult.userInfo
console.log(oauthResult);
```

## Performance considerations

When uploading large files, you may want to run the `commit` calls inside a worker, to offload the sha256 computations.
Expand Down
1 change: 1 addition & 0 deletions packages/hub/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./list-datasets";
export * from "./list-files";
export * from "./list-models";
export * from "./list-spaces";
export * from "./oauth-handle-redirect";
export * from "./oauth-login";
export * from "./parse-safetensors-metadata";
export * from "./upload-file";
Expand Down
203 changes: 203 additions & 0 deletions packages/hub/src/lib/oauth-handle-redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { HUB_URL } from "../consts";
import { createApiError } from "../error";

export interface OAuthResult {
accessToken: string;
accessTokenExpiresAt: Date;
userInfo: {
id: string;
name: string;
fullname: string;
email?: string;
emailVerified?: boolean;
avatarUrl: string;
websiteUrl?: string;
isPro: boolean;
orgs: Array<{
name: string;
isEnterprise: boolean;
}>;
};
/**
* State passed to the OAuth provider in the original request to the OAuth provider.
*/
state?: string;
/**
* Granted scope
*/
scope: string;
}

/**
* To call after the OAuth provider redirects back to the app.
*/
export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise<OAuthResult> {
if (typeof window === "undefined") {
throw new Error("oauthHandleRedirect is only available in the browser");
}

const searchParams = new URLSearchParams(window.location.search);

const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")];

if (error) {
throw new Error(`${error}: ${errorDescription}`);
}

const code = searchParams.get("code");
const nonce = localStorage.getItem("huggingface.co:oauth:nonce");

if (!code) {
throw new Error("Missing oauth code from query parameters in redirected URL");
}

if (!nonce) {
throw new Error("Missing oauth nonce from localStorage");
}

const codeVerifier = localStorage.getItem("huggingface.co:oauth:code_verifier");

if (!codeVerifier) {
throw new Error("Missing oauth code_verifier from localStorage");
}

const state = searchParams.get("state");

if (!state) {
throw new Error("Missing oauth state from query parameters in redirected URL");
}

let parsedState: { nonce: string; redirectUri: string; state?: string };

try {
parsedState = JSON.parse(state);
} catch {
throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: " + state);
}

if (parsedState.nonce !== nonce) {
throw new Error("Invalid oauth state in redirected URL");
}

const hubUrl = opts?.hubUrl || HUB_URL;

const openidConfigUrl = `${new URL(hubUrl).origin}/.well-known/openid-configuration`;
const openidConfigRes = await fetch(openidConfigUrl, {
headers: {
Accept: "application/json",
},
});

if (!openidConfigRes.ok) {
throw await createApiError(openidConfigRes);
}

const opendidConfig: {
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint: string;
} = await openidConfigRes.json();

const tokenRes = await fetch(opendidConfig.token_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: parsedState.redirectUri,
code_verifier: codeVerifier,
}).toString(),
});

localStorage.removeItem("huggingface.co:oauth:code_verifier");
localStorage.removeItem("huggingface.co:oauth:nonce");

if (!tokenRes.ok) {
throw await createApiError(tokenRes);
}

const token: {
access_token: string;
expires_in: number;
id_token: string;
// refresh_token: string;
scope: string;
token_type: string;
} = await tokenRes.json();

const accessTokenExpiresAt = new Date(Date.now() + token.expires_in * 1000);

const userInfoRes = await fetch(opendidConfig.userinfo_endpoint, {
headers: {
Authorization: `Bearer ${token.access_token}`,
},
});

if (!userInfoRes.ok) {
throw await createApiError(userInfoRes);
}

const userInfo: {
sub: string;
name: string;
preferred_username: string;
email_verified?: boolean;
email?: string;
picture: string;
website?: string;
isPro: boolean;
orgs?: Array<{
name: string;
isEnterprise: boolean;
}>;
} = await userInfoRes.json();

return {
accessToken: token.access_token,
accessTokenExpiresAt,
userInfo: {
id: userInfo.sub,
name: userInfo.name,
fullname: userInfo.preferred_username,
email: userInfo.email,
emailVerified: userInfo.email_verified,
avatarUrl: userInfo.picture,
websiteUrl: userInfo.website,
isPro: userInfo.isPro,
orgs: userInfo.orgs || [],
},
state: parsedState.state,
scope: token.scope,
};
}

// if (code && !nonce) {
// console.warn("Missing oauth nonce from localStorage");
// }

export async function oauthHandleRedirectIfPresent(opts?: { hubUrl?: string }): Promise<OAuthResult | false> {
if (typeof window === "undefined") {
throw new Error("oauthHandleRedirect is only available in the browser");
}

const searchParams = new URLSearchParams(window.location.search);

if (searchParams.has("error")) {
return oauthHandleRedirect(opts);
}

if (searchParams.has("code")) {
if (!localStorage.getItem("huggingface.co:oauth:nonce")) {
console.warn(
"Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."
);
return false;
}

return oauthHandleRedirect(opts);
}

return false;
}
Loading

0 comments on commit dbf0327

Please sign in to comment.