Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: support the OAuth2 device flow with GitHub for signing in#16585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
hugodutka merged 5 commits intomainfromhugodutka/github-oauth2-device-flow
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
PrevPrevious commit
NextNext commit
github oauth2 device flow frontend
  • Loading branch information
@hugodutka
hugodutka committedFeb 21, 2025
commit8a1bef4e91826cefca20ffe22b48bd372e5970d2
23 changes: 23 additions & 0 deletionssite/src/api/api.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1605,6 +1605,29 @@ class ApiMethods {
return resp.data;
};

getOAuth2GitHubDeviceFlowCallback = async (
code: string,
state: string,
): Promise<TypesGen.OAuth2DeviceFlowCallbackResponse> => {
const resp = await this.axios.get(
`/api/v2/users/oauth2/github/callback?code=${code}&state=${state}`,
);
// sanity check
if (
typeof resp.data !== "object" ||
typeof resp.data.redirect_url !== "string"
) {
console.error("Invalid response from OAuth2 GitHub callback", resp);
throw new Error("Invalid response from OAuth2 GitHub callback");
}
return resp.data;
};

getOAuth2GitHubDevice = async (): Promise<TypesGen.ExternalAuthDevice> => {
const resp = await this.axios.get("/api/v2/users/oauth2/github/device");
return resp.data;
};

getOAuth2ProviderApps = async (
filter?: TypesGen.OAuth2ProviderAppFilter,
): Promise<TypesGen.OAuth2ProviderApp[]> => {
Expand Down
14 changes: 14 additions & 0 deletionssite/src/api/queries/oauth2.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,20 @@ const userAppsKey = (userId: string) => appsKey.concat(userId);
const appKey = (appId: string) => appsKey.concat(appId);
const appSecretsKey = (appId: string) => appKey(appId).concat("secrets");

export const getGitHubDevice = () => {
return {
queryKey: ["oauth2-provider", "github", "device"],
queryFn: () => API.getOAuth2GitHubDevice(),
};
};

export const getGitHubDeviceFlowCallback = (code: string, state: string) => {
return {
queryKey: ["oauth2-provider", "github", "callback", code, state],
queryFn: () => API.getOAuth2GitHubDeviceFlowCallback(code, state),
};
};

export const getApps = (userId?: string) => {
return {
queryKey: userId ? appsKey.concat(userId) : appsKey,
Expand Down
136 changes: 136 additions & 0 deletionssite/src/components/GitDeviceAuth/GitDeviceAuth.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
import type { Interpolation, Theme } from "@emotion/react";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import AlertTitle from "@mui/material/AlertTitle";
import CircularProgress from "@mui/material/CircularProgress";
import Link from "@mui/material/Link";
import type { ApiErrorResponse } from "api/errors";
import type { ExternalAuthDevice } from "api/typesGenerated";
import { Alert, AlertDetail } from "components/Alert/Alert";
import { CopyButton } from "components/CopyButton/CopyButton";
import type { FC } from "react";

interface GitDeviceAuthProps {
externalAuthDevice?: ExternalAuthDevice;
deviceExchangeError?: ApiErrorResponse;
}

export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
externalAuthDevice,
deviceExchangeError,
}) => {
let status = (
<p css={styles.status}>
<CircularProgress size={16} color="secondary" data-chromatic="ignore" />
Checking for authentication...
</p>
);
if (deviceExchangeError) {
// See https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
switch (deviceExchangeError.detail) {
case "authorization_pending":
break;
case "expired_token":
status = (
<Alert severity="error">
The one-time code has expired. Refresh to get a new one!
</Alert>
);
break;
case "access_denied":
status = (
<Alert severity="error">Access to the Git provider was denied.</Alert>
);
break;
default:
status = (
<Alert severity="error">
<AlertTitle>{deviceExchangeError.message}</AlertTitle>
{deviceExchangeError.detail && (
<AlertDetail>{deviceExchangeError.detail}</AlertDetail>
)}
</Alert>
);
break;
}
}

// If the error comes from the `externalAuthDevice` query,
// we cannot even display the user_code.
if (deviceExchangeError && !externalAuthDevice) {
return <div>{status}</div>;
}

if (!externalAuthDevice) {
return <CircularProgress />;
}

return (
<div>
<p css={styles.text}>
Copy your one-time code:&nbsp;
<div css={styles.copyCode}>
<span css={styles.code}>{externalAuthDevice.user_code}</span>
&nbsp; <CopyButton text={externalAuthDevice.user_code} />
</div>
<br />
Then open the link below and paste it:
</p>
<div css={styles.links}>
<Link
css={styles.link}
href={externalAuthDevice.verification_uri}
target="_blank"
rel="noreferrer"
>
<OpenInNewIcon fontSize="small" />
Open and Paste
</Link>
</div>

{status}
</div>
);
};

const styles = {
text: (theme) => ({
fontSize: 16,
color: theme.palette.text.secondary,
textAlign: "center",
lineHeight: "160%",
margin: 0,
}),

copyCode: {
display: "inline-flex",
alignItems: "center",
},

code: (theme) => ({
fontWeight: "bold",
color: theme.palette.text.primary,
}),

links: {
display: "flex",
gap: 4,
margin: 16,
flexDirection: "column",
},

link: {
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 16,
gap: 8,
},

status: (theme) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
color: theme.palette.text.disabled,
}),
} satisfies Record<string, Interpolation<Theme>>;
107 changes: 2 additions & 105 deletionssite/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
import type { Interpolation, Theme } from "@emotion/react";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import RefreshIcon from "@mui/icons-material/Refresh";
import AlertTitle from "@mui/material/AlertTitle";
import CircularProgress from "@mui/material/CircularProgress";
import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip";
import type { ApiErrorResponse } from "api/errors";
import type { ExternalAuth, ExternalAuthDevice } from "api/typesGenerated";
import { Alert, AlertDetail } from "components/Alert/Alert";
import { Alert } from "components/Alert/Alert";
import { Avatar } from "components/Avatar/Avatar";
import {CopyButton } from "components/CopyButton/CopyButton";
import {GitDeviceAuth } from "components/GitDeviceAuth/GitDeviceAuth";
import { SignInLayout } from "components/SignInLayout/SignInLayout";
import { Welcome } from "components/Welcome/Welcome";
import type { FC, ReactNode } from "react";
Expand DownExpand Up@@ -141,89 +139,6 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
);
};

interface GitDeviceAuthProps {
externalAuthDevice?: ExternalAuthDevice;
deviceExchangeError?: ApiErrorResponse;
}

const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
externalAuthDevice,
deviceExchangeError,
}) => {
let status = (
<p css={styles.status}>
<CircularProgress size={16} color="secondary" data-chromatic="ignore" />
Checking for authentication...
</p>
);
if (deviceExchangeError) {
// See https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
switch (deviceExchangeError.detail) {
case "authorization_pending":
break;
case "expired_token":
status = (
<Alert severity="error">
The one-time code has expired. Refresh to get a new one!
</Alert>
);
break;
case "access_denied":
status = (
<Alert severity="error">Access to the Git provider was denied.</Alert>
);
break;
default:
status = (
<Alert severity="error">
<AlertTitle>{deviceExchangeError.message}</AlertTitle>
{deviceExchangeError.detail && (
<AlertDetail>{deviceExchangeError.detail}</AlertDetail>
)}
</Alert>
);
break;
}
}

// If the error comes from the `externalAuthDevice` query,
// we cannot even display the user_code.
if (deviceExchangeError && !externalAuthDevice) {
return <div>{status}</div>;
}

if (!externalAuthDevice) {
return <CircularProgress />;
}

return (
<div>
<p css={styles.text}>
Copy your one-time code:&nbsp;
<div css={styles.copyCode}>
<span css={styles.code}>{externalAuthDevice.user_code}</span>
&nbsp; <CopyButton text={externalAuthDevice.user_code} />
</div>
<br />
Then open the link below and paste it:
</p>
<div css={styles.links}>
<Link
css={styles.link}
href={externalAuthDevice.verification_uri}
target="_blank"
rel="noreferrer"
>
<OpenInNewIcon fontSize="small" />
Open and Paste
</Link>
</div>

{status}
</div>
);
};

export default ExternalAuthPageView;

const styles = {
Expand All@@ -235,16 +150,6 @@ const styles = {
margin: 0,
}),

copyCode: {
display: "inline-flex",
alignItems: "center",
},

code: (theme) => ({
fontWeight: "bold",
color: theme.palette.text.primary,
}),

installAlert: {
margin: 16,
},
Expand All@@ -264,14 +169,6 @@ const styles = {
gap: 8,
},

status: (theme) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
color: theme.palette.text.disabled,
}),

authorizedInstalls: (theme) => ({
display: "flex",
gap: 4,
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp