27 Commits
Author SHA1 Message Date
janishutz f66933a8ce chore: remove wakatime project name 2026-10-04 16:39:56 +02:00
janishutz 50cfdb0843 chore: wakatime project name 2026-10-02 06:58:43 +02:00
janishutz 338693388b chore: fix spelling 2026-10-01 20:52:19 +02:00
janishutz 4153ae83a4 feat: secure cookies and same site set 2026-10-01 20:51:50 +02:00
janishutz 374fda716b fix: unclean crash on provider resolution failure 2026-09-28 16:41:48 +02:00
janishutz e44d4511f2 chore: make compatible with go 1.26.0 2026-09-28 16:00:52 +02:00
janishutz 90d7076726 chore: remove code causing crash 2026-09-28 11:57:58 +02:00
janishutz 58855ed3a2 chore: revert some fixes, make it work with stubs as well 2026-09-28 11:56:19 +02:00
janishutz bb548cc2c3 fix: confused var 2026-09-28 11:51:46 +02:00
janishutz d6a551d80f fix: custom check middleware enabling 2026-09-28 11:50:26 +02:00
janishutz 6bcaff2d88 chore: logging for custom middleware 2026-09-28 11:46:46 +02:00
janishutz e6b8422a1e fix: function pointer removed
Apparently go doesn't support those
2026-09-28 11:39:39 +02:00
janishutz 6cb582a762 chore: short docs on middleware 2026-09-28 11:24:35 +02:00
janishutz e241d90f42 feat: add option to add middleware for check endpoint 2026-09-28 11:23:45 +02:00
janishutz 59f635f01e fix: logout redirects 2026-09-28 06:49:10 +02:00
janishutz d9ca3abb6c fix!: actually publish the right code 2026-09-22 17:28:18 +02:00
janishutz bc4abcf5e3 fix!: user func assignment, defaultRedirect store 2026-09-22 17:24:32 +02:00
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
8 changed files with 245 additions and 35 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.
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/janishutz/oidclogin module github.com/janishutz/oidclogin
go 1.27.1 go 1.26.0
require ( require (
github.com/coreos/go-oidc/v3 v3.21.0 github.com/coreos/go-oidc/v3 v3.21.0
+39 -9
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")
@@ -35,7 +35,6 @@ func CallbackHandler(c *gin.Context) {
if c.Query("state") != state || codeVerifier == nil { if c.Query("state") != state || codeVerifier == nil {
log.Println("State invalid or verifier was not stored") log.Println("State invalid or verifier was not stored")
// TODO: Proper pages
c.HTML(500, "oidcerror.tmpl", gin.H{ c.HTML(500, "oidcerror.tmpl", gin.H{
"error": "ERR_INVALID_STATE", "error": "ERR_INVALID_STATE",
}) })
@@ -83,14 +82,20 @@ 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 verification failed (nonce missing)")
c.AbortWithStatus(500) c.HTML(500, "oidcerror.tmpl", gin.H{
"error": "ERR_AUTH",
})
c.Abort()
return return
} }
@@ -106,6 +111,14 @@ 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.Options(sessions.Options{
Path: "/",
SameSite: sameSiteMode,
HttpOnly: true,
Secure: prod,
MaxAge: 172800, // Expires in 2 days
})
session.Save() session.Save()
if redir != nil { if redir != nil {
@@ -115,10 +128,29 @@ func CallbackHandler(c *gin.Context) {
} }
} }
func logoutHandler(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
session.Save()
redir := ""
if c.Query("returnTo") != "" {
redir = c.Query("returnTo")
}
if redir != "" {
c.Redirect(307, redir)
} else {
c.Redirect(307, defaultRedirect)
}
}
// 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 +158,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
} }
+64 -8
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"log" "log"
"net/http"
"os" "os"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
@@ -20,36 +21,68 @@ func createRandomString(n int) (string, error) {
return base64.URLEncoding.EncodeToString(s), nil return base64.URLEncoding.EncodeToString(s), nil
} }
type UserFunc func(userid string, name string, email string)
var ( var (
config oauth2.Config config oauth2.Config
userFunc func(userid string, name string, email string) userFunc UserFunc
verifier oidc.IDTokenVerifier verifier oidc.IDTokenVerifier
defaultRedirect string defaultRedirect string
prod bool
sameSiteMode http.SameSite
) )
func Configure(r *gin.Engine, app_url string, default_redirect string, stubs_on_unconfigured bool) { // Wraps the normal configure function, but also gives you access to change the User Function, which is called upon login.
// It is used to create or update a user.
// The check middleware may be nil, in which case a default is used. Otherwise should be a valid gin middleware, calling c.Next() if okay to proceed.
func ConfigureFull(
r *gin.Engine,
app_url string,
default_redirect string,
user_function UserFunc,
stubs_on_unconfigured bool,
check_middleware func(c *gin.Context),
production bool,
) {
userFunc = user_function
Configure(r, app_url, default_redirect, stubs_on_unconfigured, check_middleware, production)
}
// Configure and set up the login SDK
// The check middleware may be nil, in which case a default is used. Otherwise should be a valid gin middleware, calling c.Next() if okay to proceed.
func Configure(r *gin.Engine, app_url string, default_redirect string, stubs_on_unconfigured bool, check_middleware func(c *gin.Context), production bool) {
issuer := os.Getenv("OIDC_ISSUER") issuer := os.Getenv("OIDC_ISSUER")
clientID := os.Getenv("OIDC_CLIENT_ID") clientID := os.Getenv("OIDC_CLIENT_ID")
clientSecret := os.Getenv("OIDC_CLIENT_SECRET") clientSecret := os.Getenv("OIDC_CLIENT_SECRET")
defaultRedirect = default_redirect
prod = production
if prod {
sameSiteMode = http.SameSiteNoneMode
} else {
sameSiteMode = http.SameSiteDefaultMode
}
if issuer == "" || clientID == "" || clientSecret == "" { if issuer == "" || clientID == "" || clientSecret == "" {
if stubs_on_unconfigured { if stubs_on_unconfigured {
log.Println("[JHID] WARNING: OIDC not set up due to missing environment variables. Falling back to stubs") log.Println("[JHID] WARNING: OIDC not set up due to missing environment variables. Falling back to stubs")
startStubs(r) startStubs(r, check_middleware)
return return
} else { } else {
log.Fatal("[JHID] One or more requried environment variables are missing. See docs for more information") log.Fatal("[JHID] One or more requried environment variables are missing. See docs for more information")
} }
} }
provider, err := oidc.NewProvider(context.Background(), issuer) if userFunc == nil {
verifier = *provider.Verifier(&oidc.Config{ClientID: clientID}) log.Println("[JHID] WARNING: No user function defined")
defaultRedirect = default_redirect }
provider, err := oidc.NewProvider(context.Background(), issuer)
if err != nil { if err != nil {
log.Fatal("[JHID] Provider resolution failed with error", err) log.Fatal("[JHID] Provider resolution failed with error", err)
} }
verifier = *provider.Verifier(&oidc.Config{ClientID: clientID})
config = oauth2.Config{ config = oauth2.Config{
ClientID: clientID, ClientID: clientID,
ClientSecret: clientSecret, ClientSecret: clientSecret,
@@ -58,8 +91,31 @@ 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)
if check_middleware == nil {
r.GET("/auth/v2/check", EnsureLogin(false), check_finalizer)
} else {
log.Println("[JHID] Custom check middleware enabled")
r.GET("/auth/v2/check", EnsureLogin(false), check_middleware, check_finalizer)
}
r.GET("/auth/v2/logout", logoutHandler)
log.Println("[JHID] Configured successfully") log.Println("[JHID] Configured successfully")
} }
func check_finalizer(ctx *gin.Context) { ctx.JSON(200, gin.H{"success": "true"}) }
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;
}
+32 -14
View File
@@ -1,22 +1,40 @@
package oidclogin package oidclogin
import "github.com/gin-gonic/gin" import (
"log"
func startStubs(r *gin.Engine) { "github.com/gin-contrib/sessions"
r.GET("/auth/v2/login", func(ctx *gin.Context) { "github.com/gin-gonic/gin"
redir := ctx.Query("returnTo") )
if redir != "" {
ctx.Redirect(307, redir) func startStubs(r *gin.Engine, check_middleware func(c *gin.Context)) {
r.GET("/auth/v2/login", stubsHandler)
r.GET("/auth/v2/verify", stubsHandler)
if check_middleware == nil {
r.GET("/auth/v2/check", EnsureLogin(false), check_finalizer)
} else { } else {
ctx.Redirect(307, defaultRedirect) log.Println("[JHID] Custom check middleware enabled")
r.GET("/auth/v2/check", EnsureLogin(false), check_middleware, check_finalizer)
} }
}) r.GET("/auth/v2/logout", logoutHandler)
r.GET("/auth/v2/verify", func(ctx *gin.Context) { }
redir := ctx.Query("returnTo")
if redir != "" { func stubsHandler(c *gin.Context) {
ctx.Redirect(307, redir) 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)
session.Set("jhid_uid", "stubs")
session.Save()
if redir != "" {
log.Println("[JHID] Redirecting to ", redir)
c.Redirect(307, redir)
} else {
log.Println("[JHID] Redirecting to ", defaultRedirect, " (default redirect)")
c.Redirect(307, defaultRedirect)
} }
})
} }