Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/login 実装 + base64 まわり refactor #48

Merged
merged 5 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions webapp/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IdpRepository } from '../repository/idp'

import cbRoute from './cb'
import goRoute from './go'
import loginRoute from './login'
import oauthRoute from './oauth'
import tokenRoute from './token'

Expand All @@ -32,6 +33,7 @@ app.use(async (c, next) => {
app.route('/token', tokenRoute)
app.route('/go', goRoute)
app.route('/cb', cbRoute)
app.route('/login', loginRoute)
app.route('/oauth', oauthRoute)

export default app
156 changes: 156 additions & 0 deletions webapp/api/login/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { zValidator } from '@hono/zod-validator'
import { createAppAuth } from '@octokit/auth-app'
import { Hono } from 'hono'
import { HonoEnv } from 'load-context'
import { Octokit } from 'octokit'
import { binaryToBase64 } from 'utils/convert-bin-base64'
import cookieSessionStorage from 'utils/session.server'
import { z } from 'zod'

const app = new Hono<HonoEnv>()

app.get('/', async c => {
const state = binaryToBase64(crypto.getRandomValues(new Uint8Array(30)))

const { getSession, commitSession } = cookieSessionStorage(c.env)
const session = await getSession(c.req.raw.headers.get('Cookie'))
session.flash('state', state)
c.header('Set-Cookie', await commitSession(session))

// ref: https://docs.github.com/ja/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
const oauthUrl = new URL('https://github.com/login/oauth/authorize')
const oauthParams = new URLSearchParams()
oauthParams.set('client_id', c.env.GITHUB_OAUTH_ID)
oauthParams.set('redirect_uri', `${c.env.CF_PAGES_URL}/login/github/callback`)
oauthParams.set('scope', 'read:user')
oauthParams.set('state', state)
oauthParams.set('allow_signup', 'false')

return c.redirect(oauthUrl.toString() + '?' + oauthParams.toString(), 302)
})

app.all('/', async c => {
return c.text('method not allowed', 405)
})

interface GitHubOAuthTokenResponse {
access_token: string
scope: string
token_type: string
}

// TODO: cookieSessionStorage の userId 以外は使ってないので消す
app.get(
'/callback',
zValidator('query', z.object({ code: z.string(), state: z.string() })),
async c => {
const { code, state } = c.req.valid('query')

const { getSession, commitSession } = cookieSessionStorage(c.env)
const session = await getSession(c.req.raw.headers.get('Cookie'))

if (state !== session.get('state')) {
c.header('Set-Cookie', await commitSession(session))
return c.text('state mismatch', 400)
}

const continueTo = session.get('continue_to')
if (!continueTo) {
c.header('Set-Cookie', await commitSession(session))
return c.text('continue_to not found', 400)
}

const { access_token } = await fetch(
'https://github.com/login/oauth/access_token',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: c.env.GITHUB_OAUTH_ID,
client_secret: c.env.GITHUB_OAUTH_SECRET,
code,
}),
},
)
.then(res => res.json<GitHubOAuthTokenResponse>())
.catch(() => ({ access_token: null }))

if (!access_token) {
return c.text('invalid code', 400)
}

// ----- メンバーの所属判定 ----- //
const userOctokit = new Octokit({ auth: access_token })
const appOctokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: c.env.GITHUB_APP_ID,
privateKey: atob(c.env.GITHUB_APP_PRIVKEY),
installationId: '41674415',
},
})

const { data: user } = await userOctokit.request('GET /user')

let isMember = false
try {
const checkIsOrgMemberRes = await appOctokit.request(
'GET /orgs/{org}/members/{username}',
{
org: 'saitamau-maximum',
username: user.login,
},
)
isMember = (checkIsOrgMemberRes.status as number) === 204
} catch {
isMember = false
}

if (!isMember) {
// いったん member じゃない場合はログインさせないようにする
// TODO: IdP が出来たらこっちも対応できるようにする
c.header('Set-Cookie', await commitSession(session))
return c.text('not a member', 403)
}

// すでになければ DB にユーザー情報を格納
const oauthConnInfo = await c.var.idpClient.getUserIdByOauthId(
1,
String(user.id),
)
if (!oauthConnInfo) {
const uuid = crypto.randomUUID().replaceAll('-', '')
a01sa01to marked this conversation as resolved.
Show resolved Hide resolved
// とりあえず仮情報で埋める
await c.var.idpClient.createUserWithOauth(
{
id: uuid,
display_name: user.login,
profile_image_url: user.avatar_url,
},
{
user_id: uuid,
provider_id: 1,
provider_user_id: String(user.id),
email: user.email,
name: user.login,
profile_image_url: user.avatar_url,
},
)
session.set('user_id', uuid)
} else {
session.set('user_id', oauthConnInfo.user_id)
}

c.header('Set-Cookie', await commitSession(session))
return c.redirect(continueTo, 302)
},
)

app.all('/callback', async c => {
return c.text('method not allowed', 405)
})

export default app
13 changes: 13 additions & 0 deletions webapp/api/login/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Hono } from 'hono'

import { HonoEnv } from '../../load-context'

import loginGithubRoute from './github'
import loginIndexRoute from './login'

const app = new Hono<HonoEnv>()

app.route('/', loginIndexRoute)
app.route('/github', loginGithubRoute)

export default app
48 changes: 48 additions & 0 deletions webapp/api/login/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { HonoEnv } from 'load-context'
import cookieSessionStorage from 'utils/session.server'
import { z } from 'zod'

const app = new Hono<HonoEnv>()

app.get(
'/',
zValidator(
'query',
z.object({
continue_to: z.string().optional(),
}),
),
async c => {
const { continue_to } = c.req.valid('query')

const { getSession, commitSession } = cookieSessionStorage(c.env)
const session = await getSession(c.req.raw.headers.get('Cookie'))
a01sa01to marked this conversation as resolved.
Show resolved Hide resolved
session.flash('continue_to', continue_to || '/')
c.header('Set-Cookie', await commitSession(session))

// @sor4chi デザインたのんだ
const responseHtml = `
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログイン</title>
</head>
<body>
<h1>ログイン</h1>
<p>ログインしてください</p>
<a href="/login/github">GitHub でログイン</a>
</body>
</html>
`
return c.html(responseHtml)
},
)

app.all('/', async c => {
return c.text('method not allowed', 405)
})

export default app
9 changes: 4 additions & 5 deletions webapp/api/oauth/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { token, tokenScope } from 'db/schema'
import { Hono } from 'hono'
import { HonoEnv } from 'load-context'
import { validateAuthToken } from 'utils/auth-token.server'
import { binaryToBase64 } from 'utils/convert-bin-base64'
import cookieSessionStorage from 'utils/session.server'
import { z } from 'zod'

Expand Down Expand Up @@ -135,13 +136,11 @@ app.post(
})

// code (240bit = 8bit * 30) を生成
const code = btoa(
String.fromCharCode(...crypto.getRandomValues(new Uint8Array(30))),
)
const code = binaryToBase64(crypto.getRandomValues(new Uint8Array(30)))

// access token (312bit = 8bit * 39) を生成
const accessToken = btoa(
String.fromCharCode(...crypto.getRandomValues(new Uint8Array(39))),
const accessToken = binaryToBase64(
crypto.getRandomValues(new Uint8Array(39)),
)

// DB に格納
Expand Down
5 changes: 5 additions & 0 deletions webapp/utils/convert-bin-base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const base64ToBinary = (base64: string) =>
Uint8Array.from(atob(base64), c => c.charCodeAt(0))

export const binaryToBase64 = (bin: Uint8Array) =>
btoa(String.fromCharCode(...bin))
Loading