Skip to content

Commit 0b9ddfe

Browse files
srijan-27Umang01-hasharyanmehrotra
authored
Add support for API-KEY authentication (#372)
... Co-authored-by: umang01-hash <[email protected]> Co-authored-by: mehrotra234 <[email protected]>
1 parent 3455e5c commit 0b9ddfe

File tree

7 files changed

+487
-11
lines changed

7 files changed

+487
-11
lines changed

docs/advanced-guide/auth/page.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# HTTP Authentication
2+
Authentication is a crucial aspect of web applications, controlling access to resources based on user roles or permissions.
3+
Authentication is the process of verifying a user's identity to grant access to protected resources. It ensures only
4+
authorized users can perform certain actions or access sensitive data within an application.
5+
6+
GoFr offer various approaches to implement authorization.
7+
8+
## 1. HTTP Basic Auth
9+
*Basic Authentication* is a simple HTTP authentication scheme where the user's credentials (username and password) are
10+
transmitted in the request header in a Base64-encoded format.
11+
12+
Basic auth is the simplest way to authenticate your APIs. It's built on
13+
[HTTP protocol authentication scheme](https://datatracker.ietf.org/doc/html/rfc7617). It involves sending the term
14+
`Basic` trailed by the Base64-encoded `<username>:<password>` within the standard `Authorization` header.
15+
16+
### Basic Authentication in GoFr
17+
18+
GoFr offers two ways to implement basic authentication:
19+
20+
**1. Predefined Credentials**
21+
22+
Use `EnableBasicAuth(username, password)` to configure Gofr with pre-defined credentials.
23+
24+
```go
25+
func main() {
26+
app := gofr.New()
27+
28+
app.EnableBasicAuth("admin", "secret_password") // Replace with your credentials
29+
30+
app.GET("/protected-resource", func(c *gofr.Context) (interface{}, error) {
31+
// Handle protected resource access
32+
return nil, nil
33+
})
34+
35+
app.Run()
36+
}
37+
```
38+
39+
**2. Custom Validation Function**
40+
41+
Use `EnableBasicAuthWithFunc(validationFunc)` to implement your own validation logic for credentials. The `validationFunc` takes the username and password as arguments and returns true if valid, false otherwise.
42+
43+
```go
44+
func validateUser(username string, password string) bool {
45+
// Implement your credential validation logic here
46+
// This example uses hardcoded credentials for illustration only
47+
return username == "john" && password == "doe123"
48+
}
49+
50+
func main() {
51+
app := gofr.New()
52+
53+
app.EnableBasicAuthWithFunc(validateUser)
54+
55+
app.GET("/secure-data", func(c *gofr.Context) (interface{}, error) {
56+
// Handle access to secure data
57+
return nil, nil
58+
})
59+
60+
app.Run()
61+
}
62+
```
63+
64+
### Adding Basic Authentication to HTTP Services
65+
66+
This code snippet demonstrates how to add basic authentication to an HTTP service in GoFr and make a request with the appropriate Authorization header:
67+
68+
```go
69+
app.AddHTTPService("cat-facts", "https://catfact.ninja",
70+
&service.Authentication{UserName: "abc", Password: "pass"},
71+
)
72+
```
73+
74+
75+
## 2. API Keys Auth
76+
Users include a unique API key in the request header for validation against a store of authorized keys.
77+
78+
### Usage:
79+
GoFr offers two ways to implement API Keys authentication.
80+
81+
**1. Framework Default Validation**
82+
- Users can select the framework's default validation using **_EnableAPIKeyAuth(apiKeys ...string)_**
83+
84+
```go
85+
package main
86+
87+
func main() {
88+
// initialise gofr object
89+
app := gofr.New()
90+
91+
app.EnableAPIKeyAuth("9221e451-451f-4cd6-a23d-2b2d3adea9cf", "0d98ecfe-4677-48aa-b463-d43505766915")
92+
93+
app.GET("/customer", Customer)
94+
95+
app.Run()
96+
}
97+
```
98+
99+
**2. Custom Validation Function**
100+
- Users can create their own validator function `apiKeyValidator(apiKey string) bool` for validating APIKeys and pass the func in **_EnableAPIKeyAuthWithFunc(validator)_**
101+
102+
```go
103+
package main
104+
105+
func apiKeyValidator(apiKey string) bool {
106+
validKeys := []string{"f0e1dffd-0ff0-4ac8-92a3-22d44a1464e4", "d7e4b46e-5b04-47b2-836c-2c7c91250f40"}
107+
108+
return slices.Contains(validKeys, apiKey)
109+
}
110+
111+
func main() {
112+
// initialise gofr object
113+
app := gofr.New()
114+
115+
app.EnableAPIKeyAuthWithFunc(apiKeyValidator)
116+
117+
app.GET("/customer", Customer)
118+
119+
app.Run()
120+
}
121+
```
122+
123+
### Adding Basic Authentication to HTTP Services
124+
This code snippet demonstrates how to add API Key authentication to an HTTP service in GoFr and make a request with the appropriate Authorization header:
125+
126+
```go
127+
app.AddHTTPService("http-server-using-redis", "http://localhost:8000", &service.APIKeyAuth{APIKey: "9221e451-451f-4cd6-a23d-2b2d3adea9cf"})
128+
```

docs/navigation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const navigation = [
1616
{ title: 'Publishing Custom Metrics', href: '/docs/advanced-guide/publishing-custom-metrics' },
1717
{ title: 'Custom Spans in Tracing', href: '/docs/advanced-guide/custom-spans-in-tracing' },
1818
{ title: 'HTTP Communication', href: '/docs/advanced-guide/http-communication' },
19+
{ title: 'HTTP Authentication', href: '/docs/advanced-guide/auth' },
1920
{ title: 'Circuit Breaker Support', href: '/docs/advanced-guide/circuit-breaker' },
2021
{ title: 'Monitoring Service Health', href: '/docs/advanced-guide/monitoring-service-health' },
2122
{ title: 'Handling Data Migrations', href: '/docs/advanced-guide/handling-data-migrations' },

pkg/gofr/gofr.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,6 @@ func (a *App) Migrate(migrationsMap map[int64]migration.Migrate) {
228228
migration.Run(migrationsMap, a.container)
229229
}
230230

231-
func (a *App) EnableOAuth(jwksEndpoint string, refreshInterval int) {
232-
a.AddHTTPService("gofr_oauth", jwksEndpoint)
233-
234-
oauthOption := middleware.OauthConfigs{
235-
Provider: a.container.GetHTTPService("gofr_oauth"),
236-
RefreshInterval: time.Second * time.Duration(refreshInterval),
237-
}
238-
239-
a.httpServer.router.Use(middleware.OAuth(middleware.NewOAuth(oauthOption)))
240-
}
241-
242231
func (a *App) initTracer() {
243232
tracerHost := a.Config.Get("TRACER_HOST")
244233
tracerPort := a.Config.GetOrDefault("TRACER_PORT", "9411")
@@ -293,6 +282,25 @@ func (a *App) EnableBasicAuthWithFunc(validateFunc func(username, password strin
293282
a.httpServer.router.Use(middleware.BasicAuthMiddleware(middleware.BasicAuthProvider{ValidateFunc: validateFunc}))
294283
}
295284

285+
func (a *App) EnableAPIKeyAuth(apiKeys ...string) {
286+
a.httpServer.router.Use(middleware.APIKeyAuthMiddleware(nil, apiKeys...))
287+
}
288+
289+
func (a *App) EnableAPIKeyAuthWithFunc(validator func(apiKey string) bool) {
290+
a.httpServer.router.Use(middleware.APIKeyAuthMiddleware(validator))
291+
}
292+
293+
func (a *App) EnableOAuth(jwksEndpoint string, refreshInterval int) {
294+
a.AddHTTPService("gofr_oauth", jwksEndpoint)
295+
296+
oauthOption := middleware.OauthConfigs{
297+
Provider: a.container.GetHTTPService("gofr_oauth"),
298+
RefreshInterval: time.Second * time.Duration(refreshInterval),
299+
}
300+
301+
a.httpServer.router.Use(middleware.OAuth(middleware.NewOAuth(oauthOption)))
302+
}
303+
296304
func (a *App) Subscribe(topic string, handler SubscribeFunc) {
297305
if a.container.GetSubscriber() == nil {
298306
a.container.Logger.Errorf("Subscriber not initialized in the container")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
func APIKeyAuthMiddleware(validator func(apiKey string) bool, apiKeys ...string) func(handler http.Handler) http.Handler {
8+
return func(handler http.Handler) http.Handler {
9+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
10+
authKey := r.Header.Get("X-API-KEY")
11+
if authKey == "" {
12+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
13+
return
14+
}
15+
16+
if validator != nil {
17+
if !validator(authKey) {
18+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
19+
return
20+
}
21+
} else {
22+
if !isPresent(authKey, apiKeys...) {
23+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
24+
return
25+
}
26+
}
27+
28+
handler.ServeHTTP(w, r)
29+
})
30+
}
31+
}
32+
33+
func isPresent(authKey string, apiKeys ...string) bool {
34+
for _, key := range apiKeys {
35+
if authKey == key {
36+
return true
37+
}
38+
}
39+
40+
return false
41+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package middleware
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func Test_ApiKeyAuthMiddleware(t *testing.T) {
13+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
_, _ = w.Write([]byte("Success"))
15+
})
16+
17+
validator := func(apiKey string) bool {
18+
return apiKey == "valid-key"
19+
}
20+
21+
req, err := http.NewRequestWithContext(context.Background(), "GET", "/", http.NoBody)
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
26+
testCases := []struct {
27+
desc string
28+
validator func(apiKey string) bool
29+
apiKey string
30+
responseCode int
31+
responseBody string
32+
}{
33+
{"missing api-key", nil, "", 401, "Unauthorized\n"},
34+
{"invalid api-key", nil, "invalid-key", 401, "Unauthorized\n"},
35+
{"valid api-key", nil, "valid-key-1", 200, "Success"},
36+
{"another valid api-key", nil, "valid-key-2", 200, "Success"},
37+
{"custom validator valid key", validator, "valid-key", 200, "Success"},
38+
{"custom validator in-valid key", validator, "invalid-key", 401, "Unauthorized\n"},
39+
}
40+
41+
for i, tc := range testCases {
42+
rr := httptest.NewRecorder()
43+
44+
req.Header.Set("X-API-KEY", tc.apiKey)
45+
46+
wrappedHandler := APIKeyAuthMiddleware(tc.validator, "valid-key-1", "valid-key-2")(testHandler)
47+
wrappedHandler.ServeHTTP(rr, req)
48+
49+
assert.Equal(t, tc.responseCode, rr.Code, "TEST[%d], Failed.\n%s", i, tc.desc)
50+
51+
assert.Equal(t, tc.responseBody, rr.Body.String(), "TEST[%d], Failed.\n%s", i, tc.desc)
52+
}
53+
}

pkg/gofr/service/apikey_auth.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"net/http"
6+
)
7+
8+
type APIKeyConfig struct {
9+
APIKey string
10+
}
11+
12+
func (a *APIKeyConfig) addOption(h HTTP) HTTP {
13+
return &APIKeyAuthProvider{
14+
apiKey: a.APIKey,
15+
HTTP: h,
16+
}
17+
}
18+
19+
type APIKeyAuthProvider struct {
20+
apiKey string
21+
22+
HTTP
23+
}
24+
25+
func (a *APIKeyAuthProvider) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) {
26+
return a.GetWithHeaders(ctx, path, queryParams, nil)
27+
}
28+
29+
func (a *APIKeyAuthProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{},
30+
headers map[string]string) (*http.Response, error) {
31+
setXApiKey(headers, a.apiKey)
32+
33+
return a.HTTP.GetWithHeaders(ctx, path, queryParams, headers)
34+
}
35+
36+
func (a *APIKeyAuthProvider) Post(ctx context.Context, path string, queryParams map[string]interface{},
37+
body []byte) (*http.Response, error) {
38+
return a.PostWithHeaders(ctx, path, queryParams, body, nil)
39+
}
40+
41+
func (a *APIKeyAuthProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte,
42+
headers map[string]string) (*http.Response, error) {
43+
setXApiKey(headers, a.apiKey)
44+
45+
return a.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers)
46+
}
47+
48+
func (a *APIKeyAuthProvider) Put(ctx context.Context, api string, queryParams map[string]interface{}, body []byte) (
49+
*http.Response, error) {
50+
return a.PutWithHeaders(ctx, api, queryParams, body, nil)
51+
}
52+
53+
func (a *APIKeyAuthProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte,
54+
headers map[string]string) (*http.Response, error) {
55+
setXApiKey(headers, a.apiKey)
56+
57+
return a.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers)
58+
}
59+
60+
func (a *APIKeyAuthProvider) Patch(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) (
61+
*http.Response, error) {
62+
return a.PatchWithHeaders(ctx, path, queryParams, body, nil)
63+
}
64+
65+
func (a *APIKeyAuthProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte,
66+
headers map[string]string) (*http.Response, error) {
67+
setXApiKey(headers, a.apiKey)
68+
69+
return a.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers)
70+
}
71+
72+
func (a *APIKeyAuthProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) {
73+
return a.DeleteWithHeaders(ctx, path, body, nil)
74+
}
75+
76+
func (a *APIKeyAuthProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) (
77+
*http.Response, error) {
78+
setXApiKey(headers, a.apiKey)
79+
80+
return a.HTTP.DeleteWithHeaders(ctx, path, body, headers)
81+
}
82+
83+
func setXApiKey(headers map[string]string, apiKey string) {
84+
if headers == nil {
85+
headers = make(map[string]string)
86+
}
87+
88+
headers["X-API-KEY"] = apiKey
89+
}

0 commit comments

Comments
 (0)