latest cdbcd03

oauth.go RawHistoryFolder

1package handler
2
3import (
4	"encoding/base64"
5	"encoding/json"
6	"fmt"
7	"io"
8	"net/http"
9	"os"
10
11	"github.com/bnema/gordon/internal/db"
12	"github.com/bnema/gordon/internal/db/queries"
13	"github.com/bnema/gordon/internal/server"
14	"github.com/bnema/gordon/internal/templating/render"
15	"github.com/gorilla/sessions"
16	_ "github.com/joho/godotenv/autoload"
17	"github.com/labstack/echo-contrib/session"
18	"github.com/labstack/echo/v4"
19)
20
21var urlToken string
22
23// compareGordonToken compares the token from the URL query parameter with the one from the config.yml
24func CompareGordonToken(c echo.Context, a *server.App) error {
25	configToken, err := a.Config.GetToken()
26	if err != nil {
27		return err
28	}
29
30	if urlToken != configToken {
31		return fmt.Errorf("token is empty or not valid")
32	}
33
34	return nil
35}
36
37// RenderLoginPage renders the login.html template
38func RenderLoginPage(c echo.Context, a *server.App) error {
39	urlToken = c.QueryParam("token")
40	data := map[string]interface{}{
41		"Title": "Login",
42	}
43
44	// Navigate inside the fs.FS to get the template
45	path := "html/login"
46	rendererData, err := render.GetHTMLRenderer(path, "index.gohtml", a.TemplateFS, a)
47	if err != nil {
48		return err
49	}
50
51	renderedHTML, err := rendererData.Render(data, a)
52	if err != nil {
53		return err
54	}
55
56	return c.HTML(200, renderedHTML)
57}
58
59// StartOAuthGithub starts the Github OAuth flow
60func StartOAuthGithub(c echo.Context, a *server.App) error {
61	// Check if the session is already here and valid before going for the oauth flow
62	sess, err := getSession(c)
63	if err != nil {
64		return err
65	}
66
67	// Check if the session values are valid
68	accountID, ok := sess.Values["accountID"].(string)
69	if ok && accountID != "" {
70		return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path) // StatusSeeOther is HTTP 303
71	}
72
73	//Initiate the Github OAuth flow
74	clientID := os.Getenv("GITHUB_APP_ID")
75	redirectDomain := a.GenerateOauthCallbackURL()
76	encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
77	// Redirect to Gordon's Proxy to grab the oauth access
78	oauthURL := fmt.Sprintf(
79		"https://gordon.bnema.dev/github-proxy/authorize?client_id=%s&redirect_uri=%s&state=%s",
80		clientID,
81		redirectDomain,
82		encodedState,
83	)
84	return c.Redirect(http.StatusFound, oauthURL)
85}
86
87type Sessions struct {
88	*db.Sessions
89}
90
91// OAuthCallback handles the callback response from Github OAuth
92func OAuthCallback(c echo.Context, a *server.App) error {
93	redirectPath := a.Config.Admin.Path
94
95	accessToken, encodedState, err := parseQueryParams(c)
96	if err != nil {
97		return c.String(http.StatusBadRequest, err.Error())
98	}
99
100	// update the struct with the new access token
101	a.DBTables.Sessions.AccessToken = accessToken
102
103	// Compare the state parameter with the redirect domain
104	redirectDomain := a.GenerateOauthCallbackURL()
105	encodedRedirectDomain := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
106	if encodedState != encodedRedirectDomain {
107		return c.String(http.StatusBadRequest, "invalid state parameter")
108	}
109
110	sess, err := getSession(c)
111	if err != nil {
112		return err
113	}
114
115	browserInfo := c.Request().UserAgent()
116	userInfo, err := githubGetUserDetails(c, a)
117	if err != nil {
118		return err
119	}
120
121	user, err := handleUser(c, a, accessToken, browserInfo, userInfo)
122	if err != nil {
123		return err
124	}
125
126	err = setSessionValues(c, sess, user, a.DBTables.Account.ID, a.DBTables.Sessions.Expires, a.DBTables.Sessions.ID)
127	if err != nil {
128		return err
129	}
130
131	return c.Redirect(http.StatusFound, redirectPath)
132}
133
134// parseQueryParams parses the query parameters from the callback URL
135func parseQueryParams(c echo.Context) (string, string, error) {
136	accessToken := c.QueryParam("access_token")
137	encodedState := c.QueryParam("state")
138
139	_, err := base64.StdEncoding.DecodeString(encodedState)
140	if err != nil {
141		return "", "", fmt.Errorf("invalid state parameter")
142	}
143
144	return accessToken, encodedState, nil
145}
146
147// getSession gets the session from the context
148func getSession(c echo.Context) (*sessions.Session, error) {
149	sess, err := session.Get("session", c)
150	if err != nil {
151		return nil, echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
152	}
153	return sess, nil
154}
155
156// handleUser handles the user creation or update
157func handleUser(c echo.Context, a *server.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
158	userExists, err := queries.CheckDBUserExists(a)
159	if err != nil {
160		return nil, fmt.Errorf("could not check if user exists: %w", err)
161	}
162
163	if !userExists {
164		// if it is a new user creation we compare the gordon token
165		err := CompareGordonToken(c, a)
166		if err != nil {
167			return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is empty or not valid")
168		}
169
170		return createUser(a, accessToken, browserInfo, userInfo)
171	}
172
173	if userExists {
174		// if it is an existing user we compare the login and email to see if it is the same user
175		isGoodUser, err := queries.CheckDBUserIsGood(a, userInfo)
176		if err != nil {
177			return nil, fmt.Errorf("could not check if user is good: %w", err)
178		}
179
180		if !isGoodUser {
181			return nil, echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
182		}
183
184		if isGoodUser {
185			// Update the user
186			updatedUser, err := updateUser(a, accessToken, browserInfo, userInfo)
187			if err != nil {
188				return nil, fmt.Errorf("could not update user: %w", err)
189			}
190			return updatedUser, nil
191		}
192	}
193
194	return nil, fmt.Errorf("could not handle user")
195}
196
197// createUser creates a new user in the database
198func createUser(a *server.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
199	err := queries.CreateUser(a, accessToken, browserInfo, userInfo)
200	if err != nil {
201		return nil, fmt.Errorf("could not create user: %w", err)
202	}
203	return &a.DBTables.User, nil
204}
205
206// updateUser updates an existing user in the database
207func updateUser(a *server.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
208	user, err := queries.UpdateUser(a, accessToken, browserInfo, userInfo)
209	if err != nil {
210		return nil, fmt.Errorf("could not update user: %w", err)
211	}
212	return user, nil
213}
214
215// setSessionValues sets the session values for the user
216func setSessionValues(c echo.Context, sess *sessions.Session, user *db.User, accountID string, expires string, sessionID string) error {
217	sess.Values["authenticated"] = true
218	sess.Values["accountID"] = accountID
219	sess.Values["expires"] = expires
220	sess.Values["sessionID"] = sessionID
221
222	if err := sess.Save(c.Request(), c.Response()); err != nil {
223		return fmt.Errorf("could not save session: %w", err)
224	}
225	return nil
226}
227
228// fetchGithubAPI fetches the Github API
229func fetchGithubAPI(url string, authHeader string, result interface{}) error {
230	req, err := http.NewRequest("GET", url, nil)
231	if err != nil {
232		return err
233	}
234
235	req.Header.Set("Authorization", authHeader)
236
237	resp, err := http.DefaultClient.Do(req)
238	if err != nil {
239		return err
240	}
241	defer resp.Body.Close()
242
243	if resp.StatusCode != http.StatusOK {
244		return fmt.Errorf("got status code %d", resp.StatusCode)
245	}
246
247	body, err := io.ReadAll(resp.Body)
248	if err != nil {
249		return err
250	}
251
252	err = json.Unmarshal(body, &result)
253	if err != nil {
254		return err
255	}
256
257	return nil
258}
259
260// githubGetUserDetails gets the user details from the Github API using the access token
261func githubGetUserDetails(c echo.Context, a *server.App) (userInfo *queries.GithubUserInfo, err error) {
262	accessToken := c.QueryParam("access_token")
263
264	// Fetch user info
265	err = fetchGithubAPI("https://api.github.com/user", "Bearer "+accessToken, &userInfo)
266	if err != nil {
267		return nil, err
268	}
269	// Fetch user emails
270	var emailObjects []struct {
271		Email string `json:"email"`
272	}
273	err = fetchGithubAPI("https://api.github.com/user/emails", "Bearer "+accessToken, &emailObjects)
274	if err != nil {
275		return nil, err
276	}
277
278	// Extract emails and populate the Emails field in userInfo
279	for _, emailObj := range emailObjects {
280		userInfo.Emails = append(userInfo.Emails, emailObj.Email)
281	}
282
283	return userInfo, nil
284}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close