-
Couldn't load subscription status.
- Fork 1.7k
Add OIDC middleware #2138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
meher745
wants to merge
9
commits into
gofr-dev:development
Choose a base branch
from
meher745:add-oidc-middleware
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add OIDC middleware #2138
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
d084809
feat: add OpenID Connect middleware with dynamic discovery and userin…
meher745 6f16546
refactor: update OIDC middleware & discovery per review feedback
meher745 5955516
Merge branch 'development' into add-oidc-middleware
meher745 9f29991
Merge branch 'development' into add-oidc-middleware
meher745 19458af
Refactor OIDC code, fix linter issues, add documentation
meher745 6c7a0af
Refactor OIDC code, fix linter issues, add documentation
meher745 6a73f39
Merge remote-tracking branch 'origin/development' into add-oidc-middl…
meher745 84a5b4c
Merge branch 'development' into add-oidc-middleware
meher745 102ec9e
Merge branch 'development' into add-oidc-middleware
meher745 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| --- | ||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| "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 | ||
| cacheDuration = 10 * time.Minute // Adjust cache TTL as needed | ||
| mu sync.Mutex | ||
| ) | ||
|
|
||
| // 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) | ||
| 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) | ||
| } | ||
|
|
||
| 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) | ||
Umang01-hash marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return &meta, nil | ||
| } | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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