-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add client API key authentication
- Loading branch information
1 parent
b6a6cb2
commit fb0484f
Showing
6 changed files
with
334 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,115 @@ | ||
package command | ||
|
||
import ( | ||
"fmt" | ||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" | ||
"github.com/cs3org/reva/v2/pkg/store" | ||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" | ||
"github.com/owncloud/ocis/v2/ocis-pkg/registry" | ||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing" | ||
"github.com/owncloud/ocis/v2/services/proxy/pkg/config" | ||
"github.com/owncloud/ocis/v2/services/proxy/pkg/config/parser" | ||
"github.com/owncloud/ocis/v2/services/proxy/pkg/logging" | ||
"github.com/owncloud/ocis/v2/services/proxy/pkg/middleware" | ||
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend" | ||
"github.com/urfave/cli/v2" | ||
microstore "go-micro.dev/v4/store" | ||
) | ||
|
||
// ClientAPIKey is the entrypoint for the client api key commands. | ||
func ClientAPIKey(cfg *config.Config) *cli.Command { | ||
return &cli.Command{ | ||
Name: "generate-client-api-key", | ||
Usage: "generate client API key", | ||
Category: "maintenance", | ||
Flags: []cli.Flag{ | ||
&cli.StringFlag{ | ||
Name: "claim", | ||
Value: "string", | ||
Usage: "claim to search for the user: userid, username or email", | ||
Required: true, | ||
}, | ||
&cli.StringFlag{ | ||
Name: "value", | ||
Value: "string", | ||
Usage: "value to search for the user", | ||
Required: true, | ||
}, | ||
}, | ||
Before: func(_ *cli.Context) error { | ||
return configlog.ReturnError(parser.ParseConfig(cfg)) | ||
}, | ||
Action: func(c *cli.Context) error { | ||
logger := logging.Configure(cfg.Service.Name, cfg.Log) | ||
|
||
s := store.Create( | ||
store.Store(cfg.ClientAPIKeyStore.Store), | ||
store.TTL(cfg.ClientAPIKeyStore.TTL), | ||
microstore.Nodes(cfg.ClientAPIKeyStore.Nodes...), | ||
microstore.Database("proxy"), | ||
microstore.Table("client_api_keys"), | ||
store.Authentication(cfg.ClientAPIKeyStore.AuthUsername, cfg.ClientAPIKeyStore.AuthPassword), | ||
) | ||
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
gatewaySelector, err := pool.GatewaySelector( | ||
cfg.Reva.Address, | ||
append( | ||
cfg.Reva.GetRevaOptions(), | ||
pool.WithRegistry(registry.GetRegistry()), | ||
pool.WithTracerProvider(traceProvider), | ||
)...) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var userProvider backend.UserBackend | ||
switch cfg.AccountBackend { | ||
case "cs3": | ||
userProvider = backend.NewCS3UserBackend( | ||
backend.WithLogger(logger), | ||
backend.WithRevaGatewaySelector(gatewaySelector), | ||
backend.WithMachineAuthAPIKey(cfg.MachineAuthAPIKey), | ||
backend.WithOIDCissuer(cfg.OIDC.Issuer), | ||
backend.WithServiceAccount(cfg.ServiceAccount), | ||
) | ||
default: | ||
logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend) | ||
} | ||
|
||
claim := c.String("claim") | ||
value := c.String("value") | ||
user, _, err := userProvider.GetUserByClaims(c.Context, claim, value) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
auth := middleware.ClientAPIKeyAuthenticator{ | ||
Logger: logger, | ||
UserProvider: userProvider, | ||
SigningKey: cfg.MachineAuthAPIKey, | ||
Store: s, | ||
} | ||
clientAPIKey, clientAPISecret, err := auth.CreateClientAPIKey() | ||
if err != nil { | ||
return err | ||
} | ||
err = auth.SaveKey(user.Id.OpaqueId, clientAPIKey) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fmt.Printf("Client API key created for %s", user.Username) | ||
fmt.Println() | ||
fmt.Printf(" id : %s", clientAPIKey) | ||
fmt.Println() | ||
fmt.Printf(" secret: %s", clientAPISecret) | ||
fmt.Println() | ||
|
||
return nil | ||
}, | ||
} | ||
} |
This file contains 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 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 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 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,136 @@ | ||
package middleware | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
revactx "github.com/cs3org/reva/v2/pkg/ctx" | ||
"github.com/golang-jwt/jwt" | ||
"github.com/google/uuid" | ||
"github.com/owncloud/ocis/v2/ocis-pkg/log" | ||
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend" | ||
"go-micro.dev/v4/store" | ||
"net/http" | ||
) | ||
|
||
// ClientAPIKeyAuthenticator is the authenticator responsible for client API keys. | ||
type ClientAPIKeyAuthenticator struct { | ||
Logger log.Logger | ||
UserProvider backend.UserBackend | ||
SigningKey string | ||
Store store.Store | ||
} | ||
|
||
type ClientAPIKeyRecord struct { | ||
UserId string | ||
Scopes []string | ||
} | ||
|
||
func (m ClientAPIKeyAuthenticator) CreateClientAPIKey() (string, string, error) { | ||
k := uuid.New() | ||
signature, err := jwt.SigningMethodHS256.Sign(k.String(), []byte(m.SigningKey)) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
return k.String(), signature, nil | ||
} | ||
|
||
func (m ClientAPIKeyAuthenticator) SaveKey(userId string, keyid string) error { | ||
record := ClientAPIKeyRecord{ | ||
UserId: userId, | ||
} | ||
bytes, err := json.Marshal(record) | ||
if err != nil { | ||
return err | ||
} | ||
r := store.Record{ | ||
Key: keyid, | ||
Value: bytes, | ||
} | ||
return m.Store.Write(&r) | ||
} | ||
|
||
// Authenticate implements the authenticator interface to authenticate requests via basic auth. | ||
func (m ClientAPIKeyAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) { | ||
if isPublicPath(r.URL.Path) { | ||
// The authentication of public path requests is handled by another authenticator. | ||
// Since we can't guarantee the order of execution of the authenticators, we better | ||
// implement an early return here for paths we can't authenticate in this authenticator. | ||
return nil, false | ||
} | ||
|
||
clientApiKeyId, clientApiKeySecret, ok := r.BasicAuth() | ||
if !ok { | ||
return nil, false | ||
} | ||
|
||
// re-build JWT | ||
err := m.VerifyBasicAuth(clientApiKeyId, clientApiKeySecret) | ||
if err != nil { | ||
m.Logger.Error(). | ||
Err(err). | ||
Str("authenticator", "client_api_keys"). | ||
Str("path", r.URL.Path). | ||
Msg("failed to authenticate request") | ||
return nil, false | ||
} | ||
|
||
// lookup user data in micro store | ||
clientAPIKeyRecord, err := m.LookupUser(clientApiKeyId) | ||
if err != nil { | ||
m.Logger.Error(). | ||
Err(err). | ||
Str("authenticator", "client_api_keys"). | ||
Str("path", r.URL.Path). | ||
Msg("failed to lookup client api key record") | ||
return nil, false | ||
} | ||
|
||
_, token, err := m.UserProvider.GetUserByClaims(r.Context(), "userid", clientAPIKeyRecord.UserId) | ||
if err != nil { | ||
m.Logger.Error(). | ||
Err(err). | ||
Str("authenticator", "client_api_keys"). | ||
Str("path", r.URL.Path). | ||
Str("userid", clientAPIKeyRecord.UserId). | ||
Msg("failed to get user by userid") | ||
return nil, false | ||
} | ||
|
||
// set token in request | ||
r.Header.Set(revactx.TokenHeader, token) | ||
|
||
m.Logger.Debug(). | ||
Str("authenticator", "client_api_keys"). | ||
Str("path", r.URL.Path). | ||
Msg("successfully authenticated request") | ||
return r, true | ||
} | ||
|
||
func (m ClientAPIKeyAuthenticator) VerifyBasicAuth(username string, password string) error { | ||
signature, err := jwt.SigningMethodHS256.Sign(username, []byte(m.SigningKey)) | ||
if err != nil { | ||
return err | ||
} | ||
if signature != password { | ||
return errors.New("client api key secret not matching the signature") | ||
} | ||
return nil | ||
} | ||
|
||
func (m ClientAPIKeyAuthenticator) LookupUser(clientApiKeyId string) (*ClientAPIKeyRecord, error) { | ||
records, err := m.Store.Read(clientApiKeyId) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(records) < 1 { | ||
return nil, store.ErrNotFound | ||
} | ||
data := records[0].Value | ||
v := ClientAPIKeyRecord{} | ||
err = json.Unmarshal(data, &v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &v, nil | ||
} |
This file contains 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,55 @@ | ||
package middleware | ||
|
||
import ( | ||
revactx "github.com/cs3org/reva/v2/pkg/ctx" | ||
"go-micro.dev/v4/store" | ||
"net/http" | ||
"net/http/httptest" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
"github.com/stretchr/testify/mock" | ||
|
||
. "github.com/onsi/gomega" | ||
"github.com/owncloud/ocis/v2/ocis-pkg/log" | ||
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend/mocks" | ||
) | ||
|
||
var _ = Describe("Authenticating requests", Label("ClientAPIKeyAuthenticator"), func() { | ||
var authenticator ClientAPIKeyAuthenticator | ||
ub := mocks.UserBackend{} | ||
ub.On("GetUserByClaims", mock.Anything, mock.Anything, mock.Anything).Return( | ||
nil, | ||
"reva-token", | ||
nil, | ||
) | ||
|
||
BeforeEach(func() { | ||
authenticator = ClientAPIKeyAuthenticator{ | ||
Logger: log.NewLogger(), | ||
UserProvider: &ub, | ||
SigningKey: "lorwm", | ||
Store: store.NewMemoryStore(), | ||
} | ||
}) | ||
|
||
When("the request contains correct data", func() { | ||
It("should successfully authenticate", func() { | ||
// user creates client api key | ||
key, s, err := authenticator.CreateClientAPIKey() | ||
Expect(err).To(BeNil()) | ||
|
||
err = authenticator.SaveKey("einstein", key) | ||
Expect(err).To(BeNil()) | ||
|
||
// call api with client api key | ||
req := httptest.NewRequest(http.MethodGet, "http://example.com/example/path", http.NoBody) | ||
req.SetBasicAuth(key, s) | ||
|
||
req2, valid := authenticator.Authenticate(req) | ||
|
||
Expect(valid).To(Equal(true)) | ||
Expect(req2).ToNot(BeNil()) | ||
Expect(req2.Header.Get(revactx.TokenHeader)).To(Equal("reva-token")) | ||
}) | ||
}) | ||
}) |