detached 4d9166c

Github OAuth integration complete, add a requirelogin middleware who redirect to admin/login if no session

commit
4d9166c6d286f87a9f1940cb384b531ac63d3098
author
bnema · · 2023-09-16 09:57 UTC
parent
ad67f6b
signature
unsigned
5 files · +101 -42
M internal/app/app.go +1 -1
@@ -20,7 +20,7 @@ const (
2020 // If enabled replace all docker commands by podman
2121 PodmanEnable = false
2222 PodmanSock = "/run/user/1000/podman/podman.sock"
23- OauthCallbackURL = "http://localhost:1323/oauth/callback"
23+ OauthCallbackURL = "http://localhost:1323/admin/login/oauth/callback"
2424 )
2525
2626 type App struct {
@@ -20,7 +20,7 @@ const (
20 // If enabled replace all docker commands by podman20 // If enabled replace all docker commands by podman
21 PodmanEnable = false21 PodmanEnable = false
22 PodmanSock = "/run/user/1000/podman/podman.sock"22 PodmanSock = "/run/user/1000/podman/podman.sock"
23- OauthCallbackURL = "http://localhost:1323/oauth/callback"23+ OauthCallbackURL = "http://localhost:1323/admin/login/oauth/callback"
24 )24 )
25 25
26 type App struct {26 type App struct {
M internal/gotemplate/models/html/login/index.gohtml +2 -2
@@ -21,7 +21,7 @@
2121 <script src="/assets/js/custom.js"></script>
2222 </head>
2323
24-<body class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center" hx-boost="true">
24+<body class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center">
2525 <!-- Logo and Title -->
2626 <div id="logo-mascot" class="flex flex-col justify-center items-center p-4 mb-4">
2727 <div class="circle-shadow flex justify-center w-100">
@@ -41,7 +41,7 @@
4141 <h2 class="text-2xl font-semibold text-gray-800">Login</h2>
4242 <!-- GitHub OAuth -->
4343 <div class="mt-4 p-2 bg-gordonmascot-hblue text-white rounded-md flex justify-left">
44- <a href="{{ .AdminPath }}/login/github" class="flex items-center">
44+ <a href="login/oauth/github" class="flex items-center">
4545 <img src="/assets/imgs/github/github-mark-white.png" alt="logo github" class="h-8 xl:h-12" />
4646 <span class="w-full p-2 text-white ">Login with GitHub</span>
4747 </a>
@@ -21,7 +21,7 @@
21 <script src="/assets/js/custom.js"></script>21 <script src="/assets/js/custom.js"></script>
22 </head>22 </head>
23 23
24-<body class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center" hx-boost="true">24+<body class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center">
25 <!-- Logo and Title -->25 <!-- Logo and Title -->
26 <div id="logo-mascot" class="flex flex-col justify-center items-center p-4 mb-4">26 <div id="logo-mascot" class="flex flex-col justify-center items-center p-4 mb-4">
27 <div class="circle-shadow flex justify-center w-100">27 <div class="circle-shadow flex justify-center w-100">
@@ -41,7 +41,7 @@
41 <h2 class="text-2xl font-semibold text-gray-800">Login</h2>41 <h2 class="text-2xl font-semibold text-gray-800">Login</h2>
42 <!-- GitHub OAuth -->42 <!-- GitHub OAuth -->
43 <div class="mt-4 p-2 bg-gordonmascot-hblue text-white rounded-md flex justify-left">43 <div class="mt-4 p-2 bg-gordonmascot-hblue text-white rounded-md flex justify-left">
44- <a href="{{ .AdminPath }}/login/github" class="flex items-center">44+ <a href="login/oauth/github" class="flex items-center">
45 <img src="/assets/imgs/github/github-mark-white.png" alt="logo github" class="h-8 xl:h-12" />45 <img src="/assets/imgs/github/github-mark-white.png" alt="logo github" class="h-8 xl:h-12" />
46 <span class="w-full p-2 text-white ">Login with GitHub</span>46 <span class="w-full p-2 text-white ">Login with GitHub</span>
47 </a>47 </a>
M internal/httpserve/handler/oauth.go +69 -34
@@ -1,7 +1,10 @@
11 package handler
22
33 import (
4+ "fmt"
5+ "io"
46 "net/http"
7+ "net/url"
58 "os"
69
710 "github.com/bnema/gordon/internal/app"
@@ -11,23 +14,9 @@ import (
1114 _ "github.com/joho/godotenv/autoload"
1215 "github.com/labstack/echo-contrib/session"
1316 "github.com/labstack/echo/v4"
14- "github.com/markbates/goth"
1517 "github.com/markbates/goth/gothic"
16- "github.com/markbates/goth/providers/github"
1718 )
1819
19-var providerNames []string
20-
21-func init() {
22- goth.UseProviders(
23- github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), app.OauthCallbackURL),
24- )
25- for _, provider := range goth.GetProviders() {
26- // Append the provider's name to the slice
27- providerNames = append(providerNames, provider.Name())
28- }
29-}
30-
3120 // RenderLoginPage renders the login.html template
3221 func RenderLoginPage(c echo.Context, a *app.App) error {
3322 lang := c.Get(middleware.LangKey).(string)
@@ -57,37 +46,83 @@ func RenderLoginPage(c echo.Context, a *app.App) error {
5746 return c.HTML(200, renderedHTML)
5847 }
5948
60-// StartOAuth initiates OAuth process
61-func StartOAuth(c echo.Context, a *app.App) error {
62- provider := c.Param("provider")
63- if provider == "" {
64- return echo.NewHTTPError(http.StatusBadRequest, "Missing provider")
49+func StartOAuthGithub(c echo.Context, a *app.App) error {
50+ clientID := os.Getenv("GITHUB_CLIENTID")
51+ redirectURI := a.OauthCallbackURL // Make sure this URL is registered in your GitHub OAuth App
52+ fmt.Print(a.OauthCallbackURL)
53+ // Define the scopes you need; for example, "repo" and "admin:repo_hook" for repository and webhook actions
54+ scopes := "repo,admin:repo_hook"
55+
56+ // Redirect to GitHub's OAuth page
57+ oauthURL := fmt.Sprintf(
58+ "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s",
59+ clientID,
60+ redirectURI,
61+ scopes,
62+ )
63+ return c.Redirect(http.StatusFound, oauthURL)
64+}
65+
66+func OAuthCallback(c echo.Context, a *app.App) error {
67+ clientID := os.Getenv("GITHUB_CLIENTID")
68+ clientSecret := os.Getenv("GITHUB_TOKEN")
69+ code := c.QueryParam("code")
70+
71+ if code == "" {
72+ return c.JSON(http.StatusBadRequest, "Missing code")
6573 }
6674
67- // Get the current session from echo-contrib/session
68- sess, _ := session.Get("session", c)
75+ // Create POST request to exchange code for access token
76+ payload := url.Values{}
77+ payload.Set("client_id", clientID)
78+ payload.Set("client_secret", clientSecret)
79+ payload.Set("code", code)
6980
70- // Set the provider in the session
71- sess.Values["provider"] = provider
72- sess.Save(c.Request(), c.Response())
81+ resp, err := http.PostForm("https://github.com/login/oauth/access_token", payload)
82+ if err != nil {
83+ return c.JSON(http.StatusInternalServerError, "Failed to get access token")
84+ }
85+ defer resp.Body.Close()
7386
74- gothic.BeginAuthHandler(c.Response(), c.Request())
75- return nil
76-}
87+ if resp.StatusCode != 200 {
88+ return c.JSON(http.StatusInternalServerError, "Received non-200 status code")
89+ }
7790
78-// OAuthCallback handles OAuth callback from provider
79-func OAuthCallback(c echo.Context, a *app.App) error {
80- _, err := gothic.CompleteUserAuth(c.Response(), c.Request())
91+ // Parse the response
92+ body, err := io.ReadAll(resp.Body)
93+ if err != nil {
94+ return c.JSON(http.StatusInternalServerError, "Failed to read access token")
95+ }
96+
97+ // Parse the access token from the response body
98+ accessToken := string(body)
99+
100+ parsedQuery, err := url.ParseQuery(accessToken)
81101 if err != nil {
82- return c.JSON(http.StatusUnauthorized, "Authentication failed")
102+ return c.JSON(http.StatusInternalServerError, "Failed to parse access token")
83103 }
84- // TODO: Add your logic here, like creating a session, etc.
85- return c.Redirect(http.StatusMovedPermanently, "/success")
104+
105+ actualToken := parsedQuery.Get("access_token")
106+ fmt.Println("Received access token:", actualToken)
107+
108+ // Get the current session from echo-contrib/session
109+ sess, err := session.Get("session", c)
110+ if err != nil {
111+ return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
112+ }
113+
114+ // Set the user as authenticated
115+ sess.Values["authenticated"] = true
116+ if err = sess.Save(c.Request(), c.Response()); err != nil {
117+ return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")
118+ }
119+
120+ return c.Redirect(http.StatusFound, "/admin")
121+
86122 }
87123
88124 // Logout handles user logout and clears session
89125 func Logout(c echo.Context, a *app.App) error {
90126 gothic.Logout(c.Response(), c.Request())
91- // TODO: Add your logout logic here, like clearing session, etc.
92127 return c.Redirect(http.StatusMovedPermanently, "/")
93128 }
@@ -1,7 +1,10 @@
1 package handler1 package handler
2 2
3 import (3 import (
4+ "fmt"
5+ "io"
4 "net/http"6 "net/http"
7+ "net/url"
5 "os"8 "os"
6 9
7 "github.com/bnema/gordon/internal/app"10 "github.com/bnema/gordon/internal/app"
@@ -11,23 +14,9 @@ import (
11 _ "github.com/joho/godotenv/autoload"14 _ "github.com/joho/godotenv/autoload"
12 "github.com/labstack/echo-contrib/session"15 "github.com/labstack/echo-contrib/session"
13 "github.com/labstack/echo/v4"16 "github.com/labstack/echo/v4"
14- "github.com/markbates/goth"
15 "github.com/markbates/goth/gothic"17 "github.com/markbates/goth/gothic"
16- "github.com/markbates/goth/providers/github"
17 )18 )
18 19
19-var providerNames []string
20-
21-func init() {
22- goth.UseProviders(
23- github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), app.OauthCallbackURL),
24- )
25- for _, provider := range goth.GetProviders() {
26- // Append the provider's name to the slice
27- providerNames = append(providerNames, provider.Name())
28- }
29-}
30-
31 // RenderLoginPage renders the login.html template20 // RenderLoginPage renders the login.html template
32 func RenderLoginPage(c echo.Context, a *app.App) error {21 func RenderLoginPage(c echo.Context, a *app.App) error {
33 lang := c.Get(middleware.LangKey).(string)22 lang := c.Get(middleware.LangKey).(string)
@@ -57,37 +46,83 @@ func RenderLoginPage(c echo.Context, a *app.App) error {
57 return c.HTML(200, renderedHTML)46 return c.HTML(200, renderedHTML)
58 }47 }
59 48
60-// StartOAuth initiates OAuth process49+func StartOAuthGithub(c echo.Context, a *app.App) error {
61-func StartOAuth(c echo.Context, a *app.App) error {50+ clientID := os.Getenv("GITHUB_CLIENTID")
62- provider := c.Param("provider")51+ redirectURI := a.OauthCallbackURL // Make sure this URL is registered in your GitHub OAuth App
63- if provider == "" {52+ fmt.Print(a.OauthCallbackURL)
64- return echo.NewHTTPError(http.StatusBadRequest, "Missing provider")53+ // Define the scopes you need; for example, "repo" and "admin:repo_hook" for repository and webhook actions
54+ scopes := "repo,admin:repo_hook"
55+
56+ // Redirect to GitHub's OAuth page
57+ oauthURL := fmt.Sprintf(
58+ "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s",
59+ clientID,
60+ redirectURI,
61+ scopes,
62+ )
63+ return c.Redirect(http.StatusFound, oauthURL)
64+}
65+
66+func OAuthCallback(c echo.Context, a *app.App) error {
67+ clientID := os.Getenv("GITHUB_CLIENTID")
68+ clientSecret := os.Getenv("GITHUB_TOKEN")
69+ code := c.QueryParam("code")
70+
71+ if code == "" {
72+ return c.JSON(http.StatusBadRequest, "Missing code")
65 }73 }
66 74
67- // Get the current session from echo-contrib/session75+ // Create POST request to exchange code for access token
68- sess, _ := session.Get("session", c)76+ payload := url.Values{}
77+ payload.Set("client_id", clientID)
78+ payload.Set("client_secret", clientSecret)
79+ payload.Set("code", code)
69 80
70- // Set the provider in the session81+ resp, err := http.PostForm("https://github.com/login/oauth/access_token", payload)
71- sess.Values["provider"] = provider82+ if err != nil {
72- sess.Save(c.Request(), c.Response())83+ return c.JSON(http.StatusInternalServerError, "Failed to get access token")
84+ }
85+ defer resp.Body.Close()
73 86
74- gothic.BeginAuthHandler(c.Response(), c.Request())87+ if resp.StatusCode != 200 {
75- return nil88+ return c.JSON(http.StatusInternalServerError, "Received non-200 status code")
76-}89+ }
77 90
78-// OAuthCallback handles OAuth callback from provider91+ // Parse the response
79-func OAuthCallback(c echo.Context, a *app.App) error {92+ body, err := io.ReadAll(resp.Body)
80- _, err := gothic.CompleteUserAuth(c.Response(), c.Request())93+ if err != nil {
94+ return c.JSON(http.StatusInternalServerError, "Failed to read access token")
95+ }
96+
97+ // Parse the access token from the response body
98+ accessToken := string(body)
99+
100+ parsedQuery, err := url.ParseQuery(accessToken)
81 if err != nil {101 if err != nil {
82- return c.JSON(http.StatusUnauthorized, "Authentication failed")102+ return c.JSON(http.StatusInternalServerError, "Failed to parse access token")
83 }103 }
84- // TODO: Add your logic here, like creating a session, etc.104+
85- return c.Redirect(http.StatusMovedPermanently, "/success")105+ actualToken := parsedQuery.Get("access_token")
106+ fmt.Println("Received access token:", actualToken)
107+
108+ // Get the current session from echo-contrib/session
109+ sess, err := session.Get("session", c)
110+ if err != nil {
111+ return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
112+ }
113+
114+ // Set the user as authenticated
115+ sess.Values["authenticated"] = true
116+ if err = sess.Save(c.Request(), c.Response()); err != nil {
117+ return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")
118+ }
119+
120+ return c.Redirect(http.StatusFound, "/admin")
121+
86 }122 }
87 123
88 // Logout handles user logout and clears session124 // Logout handles user logout and clears session
89 func Logout(c echo.Context, a *app.App) error {125 func Logout(c echo.Context, a *app.App) error {
90 gothic.Logout(c.Response(), c.Request())126 gothic.Logout(c.Response(), c.Request())
91- // TODO: Add your logout logic here, like clearing session, etc.
92 return c.Redirect(http.StatusMovedPermanently, "/")127 return c.Redirect(http.StatusMovedPermanently, "/")
93 }128 }
A internal/httpserve/middleware/requirelogin.go +24 -0
@@ -0,0 +1,24 @@
1+package middleware
2+
3+import (
4+ "net/http"
5+
6+ "github.com/labstack/echo-contrib/session"
7+ "github.com/labstack/echo/v4"
8+)
9+
10+func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
11+ return func(c echo.Context) error {
12+ sess, err := session.Get("session", c)
13+ if err != nil {
14+ return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
15+ }
16+
17+ // Check if the user is authenticated
18+ if sess.Values["authenticated"] == nil || sess.Values["authenticated"] == false {
19+ return c.Redirect(http.StatusSeeOther, "/admin/login")
20+ }
21+
22+ return next(c)
23+ }
24+}
@@ -0,0 +1,24 @@
1+package middleware
2+
3+import (
4+ "net/http"
5+
6+ "github.com/labstack/echo-contrib/session"
7+ "github.com/labstack/echo/v4"
8+)
9+
10+func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
11+ return func(c echo.Context) error {
12+ sess, err := session.Get("session", c)
13+ if err != nil {
14+ return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
15+ }
16+
17+ // Check if the user is authenticated
18+ if sess.Values["authenticated"] == nil || sess.Values["authenticated"] == false {
19+ return c.Redirect(http.StatusSeeOther, "/admin/login")
20+ }
21+
22+ return next(c)
23+ }
24+}
M internal/httpserve/router.go +5 -5
@@ -18,7 +18,7 @@ func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo {
1818 e.Use(middleware.SetCommonDataMiddleware(a))
1919 e.Use(middleware.ErrorHandler)
2020 // Add session middleware
21- e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SECRET")))))
21+ e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))))
2222 e.Use(middleware.ColorSchemeDetection)
2323 e.Use(middleware.LanguageDetection)
2424
@@ -44,17 +44,17 @@ func bindStaticRoute(e *echo.Echo, a *app.App, path string) {
4444 func bindAdminRoute(e *echo.Echo, a *app.App, path string) {
4545 e.GET(path, func(c echo.Context) error {
4646 return handler.AdminRoute(c, a)
47- })
47+ }, middleware.RequireLogin) // Require login
4848 }
4949
5050 func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
5151 e.GET(adminPath+"/login", func(c echo.Context) error {
5252 return handler.RenderLoginPage(c, a)
5353 })
54- e.GET(adminPath+"/login/:provider", func(c echo.Context) error {
55- return handler.StartOAuth(c, a)
54+ e.GET(adminPath+"/login/oauth/github", func(c echo.Context) error {
55+ return handler.StartOAuthGithub(c, a)
5656 })
57- e.GET(a.OauthCallbackURL, func(c echo.Context) error {
57+ e.GET(adminPath+"/login/oauth/callback", func(c echo.Context) error {
5858 return handler.OAuthCallback(c, a)
5959 })
6060 e.GET(adminPath+"/logout", func(c echo.Context) error {
@@ -18,7 +18,7 @@ func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo {
18 e.Use(middleware.SetCommonDataMiddleware(a))18 e.Use(middleware.SetCommonDataMiddleware(a))
19 e.Use(middleware.ErrorHandler)19 e.Use(middleware.ErrorHandler)
20 // Add session middleware20 // Add session middleware
21- e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SECRET")))))21+ e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))))
22 e.Use(middleware.ColorSchemeDetection)22 e.Use(middleware.ColorSchemeDetection)
23 e.Use(middleware.LanguageDetection)23 e.Use(middleware.LanguageDetection)
24 24
@@ -44,17 +44,17 @@ func bindStaticRoute(e *echo.Echo, a *app.App, path string) {
44 func bindAdminRoute(e *echo.Echo, a *app.App, path string) {44 func bindAdminRoute(e *echo.Echo, a *app.App, path string) {
45 e.GET(path, func(c echo.Context) error {45 e.GET(path, func(c echo.Context) error {
46 return handler.AdminRoute(c, a)46 return handler.AdminRoute(c, a)
47- })47+ }, middleware.RequireLogin) // Require login
48 }48 }
49 49
50 func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {50 func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
51 e.GET(adminPath+"/login", func(c echo.Context) error {51 e.GET(adminPath+"/login", func(c echo.Context) error {
52 return handler.RenderLoginPage(c, a)52 return handler.RenderLoginPage(c, a)
53 })53 })
54- e.GET(adminPath+"/login/:provider", func(c echo.Context) error {54+ e.GET(adminPath+"/login/oauth/github", func(c echo.Context) error {
55- return handler.StartOAuth(c, a)55+ return handler.StartOAuthGithub(c, a)
56 })56 })
57- e.GET(a.OauthCallbackURL, func(c echo.Context) error {57+ e.GET(adminPath+"/login/oauth/callback", func(c echo.Context) error {
58 return handler.OAuthCallback(c, a)58 return handler.OAuthCallback(c, a)
59 })59 })
60 e.GET(adminPath+"/logout", func(c echo.Context) error {60 e.GET(adminPath+"/logout", func(c echo.Context) error {

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