import * as cookie from 'https://deno.land/std@0.188.0/http/cookie.ts' export class Auth { constructor({ baseUrl, remoteBaseUrl, giteaAppBaseUrl, giteaApiBaseUrl, giteaWebBaseUrl, giteaClientId, giteaClientSecret }) { this.baseUrl = baseUrl this.remoteBaseUrl = remoteBaseUrl this.giteaAppBaseUrl = giteaAppBaseUrl this.giteaApiBaseUrl = giteaApiBaseUrl this.giteaWebBaseUrl = giteaWebBaseUrl this.giteaClientId = giteaClientId this.giteaClientSecret = giteaClientSecret } redirectUrl(state) { const url = new URL( this.giteaWebBaseUrl + '/login/oauth/authorize' ) const search = new URLSearchParams() search.set('response_type', 'code') search.set('client_id', this.giteaClientId) search.set('redirect_uri', this.callbackUrl) search.set('state', state) url.search = search.toString() return url.toString() } buildState() { const timestamp = new Date().valueOf() const randomInt = Math.floor(Math.random() * 10000) // TODO: sign return `${randomInt}-${timestamp}` } get callbackUrl() { return this.remoteBaseUrl + '/api/auth/callback' } async redirect(event) { const state = this.buildState() const url = this.redirectUrl(state) const headers = new Headers({ Location: url }) cookie.setCookie(headers, { name: 'oauth.gitea.state', value: state, }) event.respondWith(new Response('', { headers, status: 302, })) } get tokenEndpoint() { return ( this.giteaAppBaseUrl + '/login/oauth/access_token' ) } get userinfoEndpoint() { return ( this.giteaAppBaseUrl + '/login/oauth/userinfo' ) } async getToken(code) { const resp = await fetch(this.tokenEndpoint, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ grant_type: 'authorization_code', code, client_id: this.giteaClientId, client_secret: this.giteaClientSecret, redirect_uri: this.callbackUrl, }), }) return await resp.json() } saveTokens(headers, data) { cookie.setCookie(headers, { name: 'oauth.gitea.accessToken', value: data.access_token, }) cookie.setCookie(headers, { name: 'oauth.gitea.refreshToken', value: data.refresh_token, }) cookie.setCookie(headers, { name: 'oauth.gitea.expires', value: String( Math.floor(new Date().valueOf() / 1000) + data.expires_in ), }) } async callback(event) { const url = new URL(event.request.url) const { state, code } = Object.fromEntries( url.searchParams.entries() ) const cookies = cookie.getCookies( event.request.headers ) const headers = new Headers({ Location: '/#/' }) if (cookies['oauth.gitea.state'] !== state) { event.respondWith(new Response('invalid state', { status: 401, })) return } const data = await this.getToken(code) cookie.deleteCookie(headers, 'oauth.gitea.state') this.saveTokens(headers, data) event.respondWith(new Response('', { headers, status: 302, })) } async refreshToken(refresh_token) { const resp = await fetch(this.tokenEndpoint, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ refresh_token, grant_type: 'refresh_token', }) }) return resp.ok } async refresh(event) { const headers = new Headers() const cookies = cookie.getCookies( event.request.headers ) const token = cookies['oauth.gitea.refreshToken'] const data = await this.refreshToken(token) this.saveTokens(headers, data) event.respondWith( new Response(JSON.stringify({}), {headers}) ) } async serve(event) { const {pathname} = new URL(event.request.url) const u = this.baseUrl if (pathname === `${u}/api/auth`) { await this.redirect(event) } else if (pathname === `${u}/api/auth/callback`) { await this.callback(event) } else if (pathname === `${u}/api/auth/refresh`) { await this.refresh(event) } else { event.respondWith(new Response( 'Not Found', {status: 404} )) } } async userInfo(token) { const resp = await fetch(this.userinfoEndpoint, { method: 'GET', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }) return { resp, json: await resp.json() } } async requireAuth(event) { const headers = new Headers() const cookies = cookie.getCookies( event.request.headers ) const token = cookies['oauth.gitea.accessToken'] if (!token) { event.respondWith(Response.json( {error: 'Token missing'}, {status: 401} )) return } const {resp, json} = await this.userInfo(token) if (!resp.ok) { const refresh = cookies['oauth.gitea.refreshToken'] if (refresh) { const data = await this.refreshToken(token) this.saveTokens(headers, data) const token = data.access_token if (token) { const {resp, json} = await this.userInfo(token) return { allow: resp.ok, headers, } } } } return { allow: resp.ok, headers, } } }