refactor: carry provider id in the OIDC state token

This commit is contained in:
2026-08-03 10:41:30 +01:00
parent e22faebfcd
commit 8f5873afca
3 changed files with 59 additions and 14 deletions
+3 -2
View File
@@ -84,7 +84,7 @@ func HandleOIDCStart(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "state gen failed"})
return
}
if err := SaveStateInstance(ctx, state, inst.InstanceID); err != nil {
if err := saveState(ctx, state, oidcState{InstanceID: inst.InstanceID, ProviderID: ""}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "state save failed"})
return
}
@@ -93,11 +93,12 @@ func HandleOIDCStart(c *gin.Context) {
func HandleOIDCCallback(c *gin.Context) {
ctx := c.Request.Context()
instanceID, ok := ConsumeStateInstance(ctx, c.Query("state"))
st, ok := consumeState(ctx, c.Query("state"))
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"})
return
}
instanceID := st.InstanceID
// The start handler checks this too, but an ungated callback is the half
// that matters: a start that refuses is a dead end, while a callback that
-12
View File
@@ -89,15 +89,3 @@ func GetSession(ctx context.Context, id string) (*Session, error) {
func DeleteSession(ctx context.Context, id string) error {
return rdb.Del(ctx, sessionPrefix+id).Err()
}
func SaveStateInstance(ctx context.Context, state, instanceID string) error {
return rdb.Set(ctx, statePrefix+state, instanceID, 10*time.Minute).Err()
}
func ConsumeStateInstance(ctx context.Context, state string) (string, bool) {
instanceID, err := rdb.GetDel(ctx, statePrefix+state).Result()
if err != nil || instanceID == "" {
return "", false
}
return instanceID, true
}
+56
View File
@@ -0,0 +1,56 @@
package auth
import (
"context"
"encoding/json"
"time"
)
// oidcState is what a login flow parks in Redis between the start redirect and
// the callback. It carries the provider as well as the instance: the callback's
// :providerId path segment is attacker-controlled, and this is the half that
// was issued by the start handler.
type oidcState struct {
InstanceID string `json:"instance_id"`
ProviderID string `json:"provider_id"`
}
func encodeState(s oidcState) (string, error) {
b, err := json.Marshal(s)
if err != nil {
return "", err
}
return string(b), nil
}
func decodeState(raw string) (oidcState, bool) {
var s oidcState
if raw == "" {
return oidcState{}, false
}
if err := json.Unmarshal([]byte(raw), &s); err != nil {
return oidcState{}, false
}
if s.InstanceID == "" || s.ProviderID == "" {
return oidcState{}, false
}
return s, true
}
func saveState(ctx context.Context, state string, s oidcState) error {
raw, err := encodeState(s)
if err != nil {
return err
}
return rdb.Set(ctx, statePrefix+state, raw, 10*time.Minute).Err()
}
// consumeState is GetDel: a state is single-use, so a replayed callback finds
// nothing and is refused.
func consumeState(ctx context.Context, state string) (oidcState, bool) {
raw, err := rdb.GetDel(ctx, statePrefix+state).Result()
if err != nil {
return oidcState{}, false
}
return decodeState(raw)
}