10 Commits
Author SHA1 Message Date
janishutz 92182271b0 fix: crash on undefined userFunc 2026-09-22 17:17:21 +02:00
janishutz fa9cc4fbcb fix: css file for error pages 2026-09-22 17:06:35 +02:00
janishutz 2ae92f8191 feat: logout endpoint 2026-09-21 16:15:40 +02:00
janishutz a9245fd20f feat: check endpoint for CORS applications 2026-09-20 11:04:48 +02:00
janishutz 996f4959fb feat: stubs also call user function on login 2026-09-20 10:59:57 +02:00
janishutz 6fb820d6af feat: store UID in session 2026-09-19 18:00:17 +02:00
janishutz 0c4a463052 chore: add usage to readme 2026-09-19 17:15:47 +02:00
janishutz d744f68b12 fix: stylesheet 2026-09-19 17:10:47 +02:00
janishutz 133cbfd86c feat: improve templates 2026-09-19 17:04:11 +02:00
janishutz da7dd13e2a feat: add error handler 2026-09-19 16:49:36 +02:00
7 changed files with 177 additions and 24 deletions

No files matched your search

+34
View File
@@ -8,3 +8,37 @@ OIDC_CLIENT_SECRET=<client secret>
OIDC_ISSUE=<issuer url> OIDC_ISSUE=<issuer url>
``` ```
If these env vars are not present, then depending on configuration, stubs will be used for the endpoints or the program will crash If these env vars are not present, then depending on configuration, stubs will be used for the endpoints or the program will crash
## Usage
```go
package main
import (
"os"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/janishutz/oidclogin"
)
func main() {
r := gin.Default()
// TODO: Better secret
store := memstore.NewStore([]byte("secret"))
r.Use(sessions.Sessions("jhid", store))
oidclogin.Configure(r, os.Getenv("APP_BASE_URL"), "/account", false)
r.LoadHTMLGlob("public/*")
r.GET("/account", oidclogin.EnsureLogin(false), func(ctx *gin.Context) {
ctx.HTML(200, "main.tmpl", gin.H{})
})
r.Run()
}
```
This example further needs the environment variable `APP_BASE_URL` set to something like `https://app.example.org`.
Further, you should create a template file called `main.tmpl` and also copy over the template files in the `public` directory here and edit them.
+24 -7
View File
@@ -10,7 +10,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
func LoginHandler(c *gin.Context) { func loginHandler(c *gin.Context) {
// Set up user session // Set up user session
state := rand.Text() state := rand.Text()
nonce := rand.Text() nonce := rand.Text()
@@ -27,7 +27,7 @@ func LoginHandler(c *gin.Context) {
c.Redirect(301, config.AuthCodeURL(state, oidc.Nonce(nonce), oauth2.S256ChallengeOption(codeVerifier))) c.Redirect(301, config.AuthCodeURL(state, oidc.Nonce(nonce), oauth2.S256ChallengeOption(codeVerifier)))
} }
func CallbackHandler(c *gin.Context) { func callbackHandler(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
state := session.Get("jhid_oauth_state") state := session.Get("jhid_oauth_state")
nonce := session.Get("jhid_oauth_nonce") nonce := session.Get("jhid_oauth_nonce")
@@ -83,19 +83,27 @@ func CallbackHandler(c *gin.Context) {
} }
if err := idToken.Claims(&claims); err != nil { if err := idToken.Claims(&claims); err != nil {
log.Println("Token claims generation failed", err) log.Println("Token claims generation failed", err)
c.AbortWithStatus(500) c.HTML(500, "oidcerror.tmpl", gin.H{
"error": "ERR_AUTH",
})
c.Abort()
return return
} }
// Verify NONCE // Verify NONCE
if nonce != claims.Nonce { if nonce != claims.Nonce {
log.Println("Token verificcation failed", err) log.Println("Token verificcation failed", err)
c.AbortWithStatus(500) c.HTML(500, "oidcerror.tmpl", gin.H{
"error": "ERR_AUTH",
})
c.Abort()
return return
} }
if userFunc != nil { if userFunc != nil {
userFunc(claims.Uid, claims.Name, claims.Email) userFunc(claims.Uid, claims.Name, claims.Email)
} else {
log.Println("[JHID] WARNING: No user function defined")
} }
// Clear session data of oauth related state // Clear session data of oauth related state
@@ -106,6 +114,7 @@ func CallbackHandler(c *gin.Context) {
session.Delete("redirect") session.Delete("redirect")
session.Set("jhid_auth", true) session.Set("jhid_auth", true)
session.Set("jhid_uid", claims.Uid)
session.Save() session.Save()
if redir != nil { if redir != nil {
@@ -115,10 +124,20 @@ func CallbackHandler(c *gin.Context) {
} }
} }
func logoutHandler(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
session.Save()
}
// Ensure that a user is currently logged in.
// If redirectFail is set, then if not, the user is redirected.
func EnsureLogin(redirectFail bool) func(c *gin.Context) { func EnsureLogin(redirectFail bool) func(c *gin.Context) {
return (func(c *gin.Context) { return (func(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
if session.Get("jhid_auth") == true { if session.Get("jhid_auth") == true {
// Set the UID on the context
c.Set("uid", session.Get("jhid_uid").(string))
c.Next() c.Next()
} else { } else {
if redirectFail { if redirectFail {
@@ -126,9 +145,7 @@ func EnsureLogin(redirectFail bool) func(c *gin.Context) {
c.Abort() c.Abort()
return return
} else { } else {
c.HTML(401, "autherror.tmpl", gin.H{ c.HTML(401, "autherror.tmpl", gin.H{})
"error": "ERR_AUTH",
})
c.Abort() c.Abort()
return return
} }
+19 -2
View File
@@ -27,6 +27,7 @@ var (
defaultRedirect string defaultRedirect string
) )
// Configure and set up the login SDK.
func Configure(r *gin.Engine, app_url string, default_redirect string, stubs_on_unconfigured bool) { func Configure(r *gin.Engine, app_url string, default_redirect string, stubs_on_unconfigured bool) {
issuer := os.Getenv("OIDC_ISSUER") issuer := os.Getenv("OIDC_ISSUER")
clientID := os.Getenv("OIDC_CLIENT_ID") clientID := os.Getenv("OIDC_CLIENT_ID")
@@ -58,8 +59,24 @@ func Configure(r *gin.Engine, app_url string, default_redirect string, stubs_on_
Scopes: []string{oidc.ScopeOpenID, "email", "profile"}, Scopes: []string{oidc.ScopeOpenID, "email", "profile"},
} }
r.GET("/auth/v2/login", LoginHandler) r.GET("/auth/v2/login", loginHandler)
r.GET("/auth/v2/verify", CallbackHandler) r.GET("/auth/v2/verify", callbackHandler)
r.GET("/auth/v2/check", EnsureLogin(false), func(ctx *gin.Context) { ctx.JSON(200, gin.H{"success": "true"}) })
r.GET("/auth/v2/logout", logoutHandler)
log.Println("[JHID] Configured successfully") log.Println("[JHID] Configured successfully")
} }
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
log.Println("Error during route:", c.Errors.Last().Err)
c.JSON(500, gin.H{
"error": "Internal Server Error",
})
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<!-- NOTE: You should definitely host this yourself! (and you can adjust the style that way, too!) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/janishutz/oidclogin/public/style.css">
</head>
<body>
<div class="message-wrapper">
<h1>401</h1>
<p>Unauthorized</p>
</div>
<p>You are not authorized to view this page. Please log in</p>
<a href="/">Back Home</a>
</body>
</html>
+10 -3
View File
@@ -1,9 +1,16 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<!-- NOTE: You should definitely host this yourself! (and you can adjust the style that way, too!) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/janishutz/oidclogin/public/style.css">
</head> </head>
<body> <body>
<h1>Login Failed!</h1> <div class="message-wrapper">
<p>{{ .error }}</p> <h1>500</h1>
<p>Internal Server Error</p>
</div>
<p>There was an error logging you in. Please try again. We are sorry for the inconvenience</p>
<a href="/">Back Home</a>
<p class="error-msg">{{ .error }}</p>
</body> </body>
</html> </html>
+50
View File
@@ -0,0 +1,50 @@
html,
body {
background-color: #050505;
color: #FFFFFF;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
}
body {
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.message-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.message-wrapper h1 {
font-size: 10rem;
}
.message-wrapper p {
font-size: 1.5rem;
}
a {
color: white;
background-color: #101066;
padding: 10px;
border-radius: 10px;
cursor: pointer;
text-decoration: none;
}
.error-msg {
font-size: 0.8rem;
position: fixed;
bottom: 10px;
left: 0;
width: 100vw;
text-align: center;
color: #888888;
}
+25 -12
View File
@@ -1,22 +1,35 @@
package oidclogin package oidclogin
import "github.com/gin-gonic/gin" import (
"log"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func startStubs(r *gin.Engine) { func startStubs(r *gin.Engine) {
r.GET("/auth/v2/login", func(ctx *gin.Context) { r.GET("/auth/v2/login", stubsHandler)
redir := ctx.Query("returnTo") r.GET("/auth/v2/verify", stubsHandler)
if redir != "" { r.GET("/auth/v2/check", EnsureLogin(false), func(ctx *gin.Context) { ctx.JSON(200, gin.H{"success": "true"}) })
ctx.Redirect(307, redir) r.GET("/auth/v2/logout", logoutHandler)
}
func stubsHandler(c *gin.Context) {
redir := c.Query("returnTo")
session := sessions.Default(c)
if userFunc != nil {
userFunc("stubs", "Stubs User", "example@example.com")
} else { } else {
ctx.Redirect(307, defaultRedirect) log.Println("[JHID] WARNING: No user function defined")
} }
}) session.Set("jhid_auth", true)
r.GET("/auth/v2/verify", func(ctx *gin.Context) { session.Set("jhid_uid", "stubs")
redir := ctx.Query("returnTo") session.Save()
if redir != "" { if redir != "" {
ctx.Redirect(307, redir) log.Println("[JHID] Redirecting to ", redir)
c.Redirect(307, redir)
} else { } else {
ctx.Redirect(307, defaultRedirect) log.Println("[JHID] Redirecting to ", defaultRedirect, " (default redirect)")
c.Redirect(307, defaultRedirect)
} }
})
} }