Skip to content
157 changes: 157 additions & 0 deletions docs/advanced-guide/oidc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@

# OpenID Connect (OIDC) Middleware in GoFr

This guide explains how to integrate OpenID Connect (OIDC) authentication support into your GoFr applications using the newly added OIDC middleware and dynamic OIDC discovery helper functions.

---

## Overview

The OIDC middleware contribution provides:

- **Dynamic OIDC Discovery:** Automatically fetch and cache OIDC provider metadata, including JWKS endpoint, issuer, and userinfo endpoint.
- **JWT Validation:** Leverages GoFr’s existing OAuth middleware (`EnableOAuth`) for Bearer token extraction, JWT parsing, signature verification, issuer/audience claim validation, and JWKS key rotation handling.
- **Userinfo Fetch Middleware:** Custom middleware that uses the valid access token to fetch user profile information from the OIDC `userinfo` endpoint and attaches it to the request context.
- **Context Helper:** Convenient function to access user info data in your route handlers.

---

## 1. Fetch OIDC Discovery Metadata

Before enabling OAuth middleware, fetch the provider’s metadata from the OIDC discovery URL (e.g., Google, Okta).

```

meta, err := middleware.FetchOIDCMetadata("https://accounts.google.com/.well-known/openid-configuration")
if err != nil {
// Handle error on startup
}

```

This returns a cached struct with:
- `meta.Issuer`
- `meta.JWKSURI`
- `meta.UserInfoEndpoint`

---

## 2. Enable OAuth Middleware with Discovered Endpoints

Configure GoFr’s built-in OAuth middleware by passing the discovered JWKS URI and issuer:

```

app.EnableOAuth(
meta.JWKSURI,
300, // JWKS refresh interval in seconds (e.g. 5 minutes)
jwt.WithIssuer(meta.Issuer),
// jwt.WithAudience("your-audience") // Optional audience check
)

```

This middleware:
- Extracts and validates Bearer tokens.
- Verifies JWT signature using JWKS keys.
- Caches JWKS and refreshes on rotation or expiry.

---

## 3. Register the OIDC Userinfo Middleware

Register the custom userinfo middleware **after** the OAuth middleware. It calls the OIDC userinfo endpoint with the verified token and attaches the response data to the request context.

```

app.UseMiddleware(middleware.OIDCUserInfoMiddleware(meta.UserInfoEndpoint))

```

---

## 4. Access User Info in Handlers

Within your route handlers, retrieve the fetched user information via the context helper:

```

userInfo, ok := middleware.GetOIDCUserInfo(ctx.Request().Context())
if !ok {
// Handle missing user info (e.g., unauthorized)
}
// Use userInfo map for claims like "email", "name", "sub", etc.

```

Example handler returning user info:

```

app.GET("/profile", func(ctx *gofr.Context) (any, error) {
userInfo, ok := middleware.GetOIDCUserInfo(ctx.Request().Context())
if !ok {
return nil, fmt.Errorf("user info not found")
}
return userInfo, nil
})

```

---

## 5. Notes & Best Practices

- **Middleware Order:** Always enable OAuth middleware (`EnableOAuth`) **before** the userinfo middleware so tokens are validated first.
- **Caching:** Discovery metadata and JWKS keys are cached and refreshed automatically to handle key rotation and endpoint changes.
- **Customization:** Use `jwt.ParserOption` to enforce additional claim validation such as audience or custom checks.
- **Error Handling:** Middleware will reject requests with invalid tokens or failed userinfo fetches.
- **Extensibility:** You can extend userinfo middleware to map profile data into your app’s user management as needed.

---

## 6. Summary

| Step | Functionality |
|-----------------------------|------------------------------------------|
| Fetch discovery metadata | `FetchOIDCMetadata` |
| Enable OAuth validation | `app.EnableOAuth` |
| Fetch and inject user info | `OIDCUserInfoMiddleware` |
| Access user info in handlers| `GetOIDCUserInfo` |

---

## 7. Example Integration Snippet

```

meta, err := middleware.FetchOIDCMetadata("https://accounts.google.com/.well-known/openid-configuration")
if err != nil {
log.Fatalf("OIDC discovery failed: %v", err)
}

app.EnableOAuth(
meta.JWKSURI,
300,
jwt.WithIssuer(meta.Issuer),
)

app.UseMiddleware(middleware.OIDCUserInfoMiddleware(meta.UserInfoEndpoint))

app.GET("/profile", func(ctx *gofr.Context) (any, error) {
userInfo, ok := middleware.GetOIDCUserInfo(ctx.Request().Context())
if !ok {
return nil, fmt.Errorf("user info not found")
}
return userInfo, nil
})

```

---

This guide covers how to use your contributed OIDC middleware cleanly and idiomatically within GoFr. For more details, check the middleware source files and tests.

---


9 changes: 7 additions & 2 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ cloud.google.com/go/compute v1.34.0 h1:+k/kmViu4TEi97NGaxAATYtpYBviOWJySPZ+ekA95
cloud.google.com/go/compute v1.34.0/go.mod h1:zWZwtLwZQyonEvIQBuIa0WvraMYK69J5eDCOw9VZU4g=
cloud.google.com/go/compute v1.37.0 h1:XxtZlXYkZXub3LNaLu90TTemcFqIU1yZ4E4q9VlR39A=
cloud.google.com/go/compute v1.37.0/go.mod h1:AsK4VqrSyXBo4SMbRtfAO1VfaMjUEjEwv1UB/AwVp5Q=
cloud.google.com/go/compute v1.38.0 h1:MilCLYQW2m7Dku8hRIIKo4r0oKastlD74sSu16riYKs=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
Expand Down Expand Up @@ -1144,13 +1145,15 @@ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
Expand All @@ -1163,6 +1166,7 @@ go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzau
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
Expand All @@ -1175,6 +1179,7 @@ go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06F
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
Expand Down Expand Up @@ -1260,7 +1265,6 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand All @@ -1286,6 +1290,7 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -1335,7 +1340,6 @@ golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down Expand Up @@ -1489,6 +1493,7 @@ google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b73
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250728155136-f173205681a0/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
Expand Down
59 changes: 59 additions & 0 deletions pkg/gofr/http/middleware/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// File: pkg/gofr/http/middleware/discovery.go

package middleware

import (
"encoding/json"

Check failure on line 6 in pkg/gofr/http/middleware/discovery.go

View workflow job for this annotation

GitHub Actions / Code Quality🎖️

File is not properly formatted (gci)
"fmt"
"net/http"
"sync"
"time"
)

// OIDCMetadata represents the parts of the OIDC discovery document you need.
type OIDCMetadata struct {
Issuer string `json:"issuer"`
JWKSURI string `json:"jwks_uri"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
}

var (
cachedMeta *OIDCMetadata
cacheExpiry time.Time

Check failure on line 22 in pkg/gofr/http/middleware/discovery.go

View workflow job for this annotation

GitHub Actions / Code Quality🎖️

cacheExpiry is a global variable (gochecknoglobals)
cacheDuration = 10 * time.Minute // Adjust cache TTL as needed

Check failure on line 23 in pkg/gofr/http/middleware/discovery.go

View workflow job for this annotation

GitHub Actions / Code Quality🎖️

cacheDuration is a global variable (gochecknoglobals)
mu sync.Mutex

Check failure on line 24 in pkg/gofr/http/middleware/discovery.go

View workflow job for this annotation

GitHub Actions / Code Quality🎖️

mu is a global variable (gochecknoglobals)
)

// FetchOIDCMetadata fetches and caches OIDC discovery metadata from the given URL.
// It returns cached data if within cache duration.
func FetchOIDCMetadata(discoveryURL string) (*OIDCMetadata, error) {
mu.Lock()
defer mu.Unlock()

// Return cached metadata if still valid
if cachedMeta != nil && time.Now().Before(cacheExpiry) {
return cachedMeta, nil
}

resp, err := http.Get(discoveryURL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And http.Get without passing a contexf doesn't sound a good idea

if err != nil {
return nil, fmt.Errorf("failed to fetch OIDC discovery metadata: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OIDC discovery: unexpected HTTP status %d", resp.StatusCode)

Check failure on line 45 in pkg/gofr/http/middleware/discovery.go

View workflow job for this annotation

GitHub Actions / Code Quality🎖️

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"OIDC discovery: unexpected HTTP status %d\", resp.StatusCode)" (err113)
}

var meta OIDCMetadata
if err := json.NewDecoder(resp.Body).Decode(&meta); err != nil {
return nil, fmt.Errorf("failed to decode OIDC discovery JSON: %w", err)
}

// Cache the fetched metadata
cachedMeta = &meta
cacheExpiry = time.Now().Add(cacheDuration)

return &meta, nil
}

Loading
Loading