chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,560 @@
|
||||
# Auth Wrapper
|
||||
|
||||
The auth wrapper package provides server and client wrappers for adding authentication and authorization to your go-micro services.
|
||||
|
||||
## Installation
|
||||
|
||||
```go
|
||||
import "go-micro.dev/v5/wrapper/auth"
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The auth wrapper consists of three main components:
|
||||
|
||||
1. **Server Wrapper** (`AuthHandler`) - Protects service endpoints
|
||||
2. **Client Wrapper** (`AuthClient`) - Adds auth tokens to requests
|
||||
3. **Metadata Helpers** - Extract/inject tokens from/to metadata
|
||||
|
||||
## Server Wrapper
|
||||
|
||||
The server wrapper enforces authentication and authorization on incoming requests.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"go-micro.dev/v5"
|
||||
"go-micro.dev/v5/auth/jwt"
|
||||
authWrapper "go-micro.dev/v5/wrapper/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create auth provider
|
||||
authProvider, _ := jwt.NewAuth()
|
||||
|
||||
// Create authorization rules
|
||||
rules := auth.NewRules()
|
||||
|
||||
// Wrap service with auth
|
||||
service := micro.NewService(
|
||||
micro.Name("myservice"),
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthHandler(authWrapper.HandlerOptions{
|
||||
Auth: authProvider,
|
||||
Rules: rules,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
service.Run()
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```go
|
||||
type HandlerOptions struct {
|
||||
// Auth provider for token verification (required)
|
||||
Auth auth.Auth
|
||||
|
||||
// Rules for authorization checks (optional)
|
||||
Rules auth.Rules
|
||||
|
||||
// SkipEndpoints is a list of endpoints that don't require auth
|
||||
// Format: "Service.Method" e.g., "Greeter.Hello"
|
||||
SkipEndpoints []string
|
||||
}
|
||||
```
|
||||
|
||||
### Auth Flow
|
||||
|
||||
For each incoming request:
|
||||
|
||||
1. **Check Skip List**: If endpoint in `SkipEndpoints`, skip auth
|
||||
2. **Extract Token**: Get `Authorization: Bearer <token>` from metadata
|
||||
3. **Verify Token**: Call `auth.Inspect(token)` to get account
|
||||
4. **Check Authorization**: Call `rules.Verify(account, resource)`
|
||||
5. **Inject Context**: Add account to context with `auth.ContextWithAccount()`
|
||||
6. **Call Handler**: Proceed to actual handler
|
||||
|
||||
**Errors:**
|
||||
- `401 Unauthorized` - Missing or invalid token
|
||||
- `403 Forbidden` - Token valid but insufficient permissions
|
||||
|
||||
### Helper Functions
|
||||
|
||||
#### AuthRequired
|
||||
|
||||
Enforce auth on all endpoints (no public endpoints):
|
||||
|
||||
```go
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthRequired(authProvider, rules),
|
||||
)
|
||||
```
|
||||
|
||||
#### PublicEndpoints
|
||||
|
||||
Allow specific endpoints to be public:
|
||||
|
||||
```go
|
||||
micro.WrapHandler(
|
||||
authWrapper.PublicEndpoints(authProvider, rules, []string{
|
||||
"Health.Check",
|
||||
"Status.Version",
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
#### AuthOptional
|
||||
|
||||
Extract auth if present but don't enforce (useful for endpoints that behave differently for authenticated users):
|
||||
|
||||
```go
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthOptional(authProvider),
|
||||
)
|
||||
```
|
||||
|
||||
With `AuthOptional`, the handler can check:
|
||||
|
||||
```go
|
||||
func (s *Service) Hello(ctx context.Context, req *Request, rsp *Response) error {
|
||||
if acc, ok := auth.AccountFromContext(ctx); ok {
|
||||
rsp.Msg = "Hello, " + acc.ID
|
||||
} else {
|
||||
rsp.Msg = "Hello, anonymous"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Client Wrapper
|
||||
|
||||
The client wrapper adds authentication tokens to outgoing requests.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"go-micro.dev/v5"
|
||||
"go-micro.dev/v5/client"
|
||||
authWrapper "go-micro.dev/v5/wrapper/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
service := micro.NewService(
|
||||
micro.Name("myclient"),
|
||||
micro.WrapClient(
|
||||
authWrapper.FromToken(token),
|
||||
),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
|
||||
// All calls now include the token
|
||||
client := pb.NewMyServiceClient("myservice", service.Client())
|
||||
rsp, err := client.SomeMethod(ctx, &pb.Request{})
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```go
|
||||
type ClientOptions struct {
|
||||
// Auth provider for token generation (optional)
|
||||
Auth auth.Auth
|
||||
|
||||
// Static token to use (optional)
|
||||
// If not provided, will try to extract from context
|
||||
Token string
|
||||
}
|
||||
```
|
||||
|
||||
### Helper Functions
|
||||
|
||||
#### FromToken
|
||||
|
||||
Use a static token for all requests:
|
||||
|
||||
```go
|
||||
client.Wrap(
|
||||
authWrapper.FromToken("eyJhbGciOi..."),
|
||||
)
|
||||
```
|
||||
|
||||
Best for:
|
||||
- Pre-generated tokens
|
||||
- Service accounts
|
||||
- Long-lived tokens
|
||||
|
||||
#### FromContext
|
||||
|
||||
Extract account from context and generate token per-request:
|
||||
|
||||
```go
|
||||
client.Wrap(
|
||||
authWrapper.FromContext(authProvider),
|
||||
)
|
||||
```
|
||||
|
||||
Best for:
|
||||
- Service-to-service auth
|
||||
- Dynamic token generation
|
||||
- Request context propagation
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
func (s *Service) HandleRequest(ctx context.Context, req *Request, rsp *Response) error {
|
||||
// Account already in context from incoming request
|
||||
|
||||
// Client wrapper extracts account and generates token
|
||||
client := pb.NewOtherService("other", s.Client())
|
||||
|
||||
// Token automatically added
|
||||
otherRsp, err := client.SomeMethod(ctx, &pb.OtherRequest{})
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata Helpers
|
||||
|
||||
Low-level helpers for working with auth tokens in metadata.
|
||||
|
||||
### TokenFromMetadata
|
||||
|
||||
Extract Bearer token from request metadata:
|
||||
|
||||
```go
|
||||
import (
|
||||
"go-micro.dev/v5/metadata"
|
||||
authWrapper "go-micro.dev/v5/wrapper/auth"
|
||||
)
|
||||
|
||||
func handler(ctx context.Context, req *Request, rsp *Response) error {
|
||||
md, _ := metadata.FromContext(ctx)
|
||||
|
||||
token, err := authWrapper.TokenFromMetadata(md)
|
||||
if err != nil {
|
||||
return err // ErrMissingToken or ErrInvalidToken
|
||||
}
|
||||
|
||||
// Use token...
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- Token string (without "Bearer " prefix)
|
||||
- `ErrMissingToken` - No Authorization header found
|
||||
- `ErrInvalidToken` - Not in "Bearer <token>" format
|
||||
|
||||
### TokenToMetadata
|
||||
|
||||
Add Bearer token to outgoing request metadata:
|
||||
|
||||
```go
|
||||
md := metadata.Metadata{}
|
||||
md = authWrapper.TokenToMetadata(md, "eyJhbGciOi...")
|
||||
|
||||
ctx := metadata.NewContext(context.Background(), md)
|
||||
|
||||
// Make RPC call with metadata
|
||||
client.Call(ctx, req, rsp)
|
||||
```
|
||||
|
||||
### AccountFromMetadata
|
||||
|
||||
Extract token and verify in one step:
|
||||
|
||||
```go
|
||||
func handler(ctx context.Context, req *Request, rsp *Response) error {
|
||||
md, _ := metadata.FromContext(ctx)
|
||||
|
||||
account, err := authWrapper.AccountFromMetadata(md, authProvider)
|
||||
if err != nil {
|
||||
return errors.Unauthorized("myservice", "invalid auth")
|
||||
}
|
||||
|
||||
// Use account...
|
||||
log.Printf("Request from: %s", account.ID)
|
||||
}
|
||||
```
|
||||
|
||||
This combines:
|
||||
1. `TokenFromMetadata(md)`
|
||||
2. `authProvider.Inspect(token)`
|
||||
|
||||
## Complete Example
|
||||
|
||||
### Server
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-micro.dev/v5"
|
||||
"go-micro.dev/v5/auth"
|
||||
"go-micro.dev/v5/auth/jwt"
|
||||
authWrapper "go-micro.dev/v5/wrapper/auth"
|
||||
)
|
||||
|
||||
type Greeter struct{}
|
||||
|
||||
func (g *Greeter) Hello(ctx context.Context, req *Request, rsp *Response) error {
|
||||
// Get authenticated account
|
||||
acc, ok := auth.AccountFromContext(ctx)
|
||||
if !ok {
|
||||
return errors.Unauthorized("greeter", "auth required")
|
||||
}
|
||||
|
||||
rsp.Msg = "Hello, " + acc.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
authProvider, _ := jwt.NewAuth()
|
||||
rules := auth.NewRules()
|
||||
|
||||
service := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
micro.WrapHandler(
|
||||
authWrapper.PublicEndpoints(authProvider, rules, []string{
|
||||
"Greeter.Health",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
pb.RegisterGreeterHandler(service.Server(), &Greeter{})
|
||||
service.Run()
|
||||
}
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-micro.dev/v5"
|
||||
authWrapper "go-micro.dev/v5/wrapper/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
service := micro.NewService(
|
||||
micro.WrapClient(
|
||||
authWrapper.FromToken(token),
|
||||
),
|
||||
)
|
||||
|
||||
client := pb.NewGreeterService("greeter", service.Client())
|
||||
rsp, _ := client.Hello(context.Background(), &pb.Request{})
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Mock Auth for Tests
|
||||
|
||||
```go
|
||||
import "go-micro.dev/v5/auth/noop"
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
// Use noop auth for testing (always grants access)
|
||||
authProvider := noop.NewAuth()
|
||||
|
||||
service := micro.NewService(
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthHandler(authWrapper.HandlerOptions{
|
||||
Auth: authProvider,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// Test your service...
|
||||
}
|
||||
```
|
||||
|
||||
### Generate Test Tokens
|
||||
|
||||
```go
|
||||
func TestWithAuth(t *testing.T) {
|
||||
authProvider := noop.NewAuth()
|
||||
|
||||
// Generate test account
|
||||
acc, _ := authProvider.Generate("test-user")
|
||||
|
||||
// Generate token
|
||||
token, _ := authProvider.Token(
|
||||
auth.WithCredentials(acc.ID, acc.Secret),
|
||||
)
|
||||
|
||||
// Use token in tests
|
||||
client := micro.NewService(
|
||||
micro.WrapClient(
|
||||
authWrapper.FromToken(token.AccessToken),
|
||||
),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Gateway
|
||||
|
||||
If you're using the HTTP gateway (`micro server`), auth is automatically integrated:
|
||||
|
||||
```bash
|
||||
# Gateway enforces auth on HTTP requests
|
||||
micro server --auth jwt
|
||||
```
|
||||
|
||||
The gateway:
|
||||
1. Extracts Bearer token from HTTP `Authorization` header
|
||||
2. Verifies token
|
||||
3. Adds account to metadata
|
||||
4. Forwards to service (service still checks with wrapper)
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Use Server Wrapper
|
||||
|
||||
Even if using gateway auth, still wrap your services:
|
||||
|
||||
```go
|
||||
// ✅ Good: Defense in depth
|
||||
micro.WrapHandler(authWrapper.AuthHandler(...))
|
||||
|
||||
// ❌ Bad: Only rely on gateway
|
||||
// (services can be called directly, bypassing gateway)
|
||||
```
|
||||
|
||||
### 2. Use Strong Auth in Production
|
||||
|
||||
```go
|
||||
// ✅ Production
|
||||
authProvider, _ := jwt.NewAuth(
|
||||
auth.Issuer("your-company"),
|
||||
auth.PrivateKey(privateKey),
|
||||
auth.PublicKey(publicKey),
|
||||
)
|
||||
|
||||
// ❌ Development only
|
||||
authProvider := noop.NewAuth()
|
||||
```
|
||||
|
||||
### 3. Scope Your Rules
|
||||
|
||||
```go
|
||||
// ✅ Good: Specific scopes
|
||||
rules.Grant(&auth.Rule{
|
||||
Scope: "admin",
|
||||
Resource: &auth.Resource{Endpoint: "Admin.*"},
|
||||
})
|
||||
|
||||
// ⚠️ Risky: Too broad
|
||||
rules.Grant(&auth.Rule{
|
||||
Scope: "*",
|
||||
Resource: &auth.Resource{Endpoint: "*"},
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Check Account in Handlers
|
||||
|
||||
```go
|
||||
// ✅ Good: Verify account exists
|
||||
func (s *Service) Delete(ctx context.Context, req *Request, rsp *Response) error {
|
||||
acc, ok := auth.AccountFromContext(ctx)
|
||||
if !ok || acc.ID != req.UserID {
|
||||
return errors.Forbidden("service", "can only delete own data")
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Use AuthOptional for Mixed Endpoints
|
||||
|
||||
```go
|
||||
// ✅ Good: Works for both auth and no-auth
|
||||
func (s *Service) GetProfile(ctx context.Context, req *Request, rsp *Response) error {
|
||||
if acc, ok := auth.AccountFromContext(ctx); ok {
|
||||
// Authenticated: return private profile
|
||||
rsp.Profile = s.getPrivateProfile(acc.ID)
|
||||
} else {
|
||||
// Public: return limited profile
|
||||
rsp.Profile = s.getPublicProfile(req.UserID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Handler receives requests without auth
|
||||
|
||||
**Check:**
|
||||
1. Is wrapper applied? `micro.WrapHandler(authWrapper.AuthHandler(...))`
|
||||
2. Is endpoint in skip list? Check `SkipEndpoints`
|
||||
3. Is service registered correctly?
|
||||
|
||||
### Issue: Client gets 401 errors
|
||||
|
||||
**Check:**
|
||||
1. Is token valid? Verify with `authProvider.Inspect(token)`
|
||||
2. Is client wrapper applied? `micro.WrapClient(authWrapper.FromToken(...))`
|
||||
3. Is token expired? Check `token.Expiry`
|
||||
|
||||
### Issue: Token extraction fails
|
||||
|
||||
**Check:**
|
||||
1. Is Authorization header present? `md.Get("Authorization")`
|
||||
2. Is format correct? Must be `Bearer <token>`
|
||||
3. Is metadata propagated? Check context
|
||||
|
||||
## API Reference
|
||||
|
||||
### Server Wrapper
|
||||
|
||||
- `AuthHandler(opts HandlerOptions) server.HandlerWrapper`
|
||||
- `PublicEndpoints(auth, rules, endpoints) HandlerOptions`
|
||||
- `AuthRequired(auth, rules) HandlerOptions`
|
||||
- `AuthOptional(auth) server.HandlerWrapper`
|
||||
|
||||
### Client Wrapper
|
||||
|
||||
- `AuthClient(opts ClientOptions) client.Wrapper`
|
||||
- `FromToken(token) client.Wrapper`
|
||||
- `FromContext(auth) client.Wrapper`
|
||||
|
||||
### Metadata Helpers
|
||||
|
||||
- `TokenFromMetadata(md) (string, error)`
|
||||
- `TokenToMetadata(md, token) Metadata`
|
||||
- `AccountFromMetadata(md, auth) (*Account, error)`
|
||||
|
||||
### Constants
|
||||
|
||||
- `MetadataKeyAuthorization` = `"Authorization"`
|
||||
- `BearerPrefix` = `"Bearer "`
|
||||
|
||||
### Errors
|
||||
|
||||
- `ErrMissingToken` - No authorization token in metadata
|
||||
- `ErrInvalidToken` - Token format invalid (not "Bearer <token>")
|
||||
|
||||
## See Also
|
||||
|
||||
- [Auth Package Documentation](/auth)
|
||||
- [JWT Auth Provider](/auth/jwt)
|
||||
- [Authorization Rules](/auth#rules)
|
||||
- [Example Usage](/examples/auth)
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
@@ -0,0 +1,144 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v6/auth"
|
||||
"go-micro.dev/v6/client"
|
||||
"go-micro.dev/v6/metadata"
|
||||
)
|
||||
|
||||
// ClientOptions for configuring the auth client wrapper
|
||||
type ClientOptions struct {
|
||||
// Auth provider for token generation
|
||||
Auth auth.Auth
|
||||
// Token to use (optional - if not provided, will be extracted from context)
|
||||
Token string
|
||||
}
|
||||
|
||||
// AuthClient returns a client Wrapper that adds authentication tokens to outgoing requests.
|
||||
//
|
||||
// For each outgoing request:
|
||||
// 1. Extracts or uses provided token
|
||||
// 2. Adds Bearer token to request metadata
|
||||
// 3. Makes the RPC call
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// client := client.NewClient(
|
||||
// client.Wrap(auth.AuthClient(auth.ClientOptions{
|
||||
// Auth: myAuthProvider,
|
||||
// Token: myToken,
|
||||
// })),
|
||||
// )
|
||||
func AuthClient(opts ClientOptions) client.Wrapper {
|
||||
return func(c client.Client) client.Client {
|
||||
return &authClient{
|
||||
Client: c,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// authClient wraps a client to add authentication
|
||||
type authClient struct {
|
||||
client.Client
|
||||
opts ClientOptions
|
||||
}
|
||||
|
||||
// Call adds authentication token to the request
|
||||
func (a *authClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
// Get token from options or context
|
||||
token := a.opts.Token
|
||||
if token == "" && a.opts.Auth != nil {
|
||||
// Try to get token from context account
|
||||
if acc, ok := auth.AccountFromContext(ctx); ok {
|
||||
// Generate token for this account
|
||||
if t, err := a.opts.Auth.Token(auth.WithCredentials(acc.ID, acc.Secret)); err == nil {
|
||||
token = t.AccessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add token to metadata if available
|
||||
if token != "" {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
md = metadata.Metadata{}
|
||||
}
|
||||
md = TokenToMetadata(md, token)
|
||||
ctx = metadata.NewContext(ctx, md)
|
||||
}
|
||||
|
||||
return a.Client.Call(ctx, req, rsp, opts...)
|
||||
}
|
||||
|
||||
// Stream adds authentication token to the stream request
|
||||
func (a *authClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
||||
// Get token from options or context
|
||||
token := a.opts.Token
|
||||
if token == "" && a.opts.Auth != nil {
|
||||
// Try to get token from context account
|
||||
if acc, ok := auth.AccountFromContext(ctx); ok {
|
||||
// Generate token for this account
|
||||
if t, err := a.opts.Auth.Token(auth.WithCredentials(acc.ID, acc.Secret)); err == nil {
|
||||
token = t.AccessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add token to metadata if available
|
||||
if token != "" {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
md = metadata.Metadata{}
|
||||
}
|
||||
md = TokenToMetadata(md, token)
|
||||
ctx = metadata.NewContext(ctx, md)
|
||||
}
|
||||
|
||||
return a.Client.Stream(ctx, req, opts...)
|
||||
}
|
||||
|
||||
// Publish adds authentication token to the publish request
|
||||
func (a *authClient) Publish(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
|
||||
// Get token from options or context
|
||||
token := a.opts.Token
|
||||
if token == "" && a.opts.Auth != nil {
|
||||
// Try to get token from context account
|
||||
if acc, ok := auth.AccountFromContext(ctx); ok {
|
||||
// Generate token for this account
|
||||
if t, err := a.opts.Auth.Token(auth.WithCredentials(acc.ID, acc.Secret)); err == nil {
|
||||
token = t.AccessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add token to metadata if available
|
||||
if token != "" {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
md = metadata.Metadata{}
|
||||
}
|
||||
md = TokenToMetadata(md, token)
|
||||
ctx = metadata.NewContext(ctx, md)
|
||||
}
|
||||
|
||||
return a.Client.Publish(ctx, msg, opts...)
|
||||
}
|
||||
|
||||
// FromToken creates a client wrapper with a static token.
|
||||
// This is useful when you have a pre-generated token and don't need the auth provider.
|
||||
func FromToken(token string) client.Wrapper {
|
||||
return AuthClient(ClientOptions{
|
||||
Token: token,
|
||||
})
|
||||
}
|
||||
|
||||
// FromContext creates a client wrapper that extracts the account from context
|
||||
// and generates a token for each request. Useful for service-to-service auth.
|
||||
func FromContext(authProvider auth.Auth) client.Wrapper {
|
||||
return AuthClient(ClientOptions{
|
||||
Auth: authProvider,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v6/auth"
|
||||
"go-micro.dev/v6/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
// MetadataKeyAuthorization is the key for the Authorization header in metadata
|
||||
MetadataKeyAuthorization = "Authorization"
|
||||
// BearerPrefix is the prefix for Bearer tokens
|
||||
BearerPrefix = "Bearer "
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingToken is returned when no authorization token is found in metadata
|
||||
ErrMissingToken = errors.New("missing authorization token in metadata")
|
||||
// ErrInvalidToken is returned when the token format is invalid
|
||||
ErrInvalidToken = errors.New("invalid token format, expected 'Bearer <token>'")
|
||||
)
|
||||
|
||||
// TokenFromMetadata extracts the Bearer token from request metadata.
|
||||
// Returns the token string without the "Bearer " prefix, or an error if not found.
|
||||
func TokenFromMetadata(md metadata.Metadata) (string, error) {
|
||||
// Check for Authorization header
|
||||
authHeader, ok := md.Get(MetadataKeyAuthorization)
|
||||
if !ok {
|
||||
// Also check lowercase version
|
||||
authHeader, ok = md.Get(strings.ToLower(MetadataKeyAuthorization))
|
||||
if !ok {
|
||||
return "", ErrMissingToken
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Bearer prefix
|
||||
if !strings.HasPrefix(authHeader, BearerPrefix) {
|
||||
return "", ErrInvalidToken
|
||||
}
|
||||
|
||||
// Extract token (remove "Bearer " prefix)
|
||||
token := strings.TrimPrefix(authHeader, BearerPrefix)
|
||||
if token == "" {
|
||||
return "", ErrInvalidToken
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// TokenToMetadata adds a Bearer token to metadata for outgoing requests.
|
||||
// The token should be provided without the "Bearer " prefix.
|
||||
func TokenToMetadata(md metadata.Metadata, token string) metadata.Metadata {
|
||||
if md == nil {
|
||||
md = metadata.Metadata{}
|
||||
}
|
||||
|
||||
// Add Bearer prefix and set in metadata
|
||||
md.Set(MetadataKeyAuthorization, BearerPrefix+token)
|
||||
return md
|
||||
}
|
||||
|
||||
// AccountFromMetadata extracts and verifies the token from metadata,
|
||||
// returning the associated account. This is a convenience function that
|
||||
// combines TokenFromMetadata and auth.Inspect.
|
||||
func AccountFromMetadata(md metadata.Metadata, a auth.Auth) (*auth.Account, error) {
|
||||
token, err := TokenFromMetadata(md)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.Inspect(token)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v6/auth"
|
||||
"go-micro.dev/v6/errors"
|
||||
"go-micro.dev/v6/metadata"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// HandlerOptions for configuring the auth handler wrapper
|
||||
type HandlerOptions struct {
|
||||
// Auth provider for token verification
|
||||
Auth auth.Auth
|
||||
// Rules for authorization checks
|
||||
Rules auth.Rules
|
||||
// SkipEndpoints is a list of endpoints that don't require auth
|
||||
// Format: "Service.Method" e.g., "Greeter.Hello"
|
||||
SkipEndpoints []string
|
||||
}
|
||||
|
||||
// AuthHandler returns a server HandlerWrapper that enforces authentication and authorization.
|
||||
//
|
||||
// For each incoming request:
|
||||
// 1. Extracts Bearer token from metadata
|
||||
// 2. Verifies token using auth.Inspect()
|
||||
// 3. Checks authorization using rules.Verify()
|
||||
// 4. Adds account to context
|
||||
// 5. Calls the handler if authorized
|
||||
//
|
||||
// Returns 401 Unauthorized if token is missing/invalid.
|
||||
// Returns 403 Forbidden if account lacks necessary permissions.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// service := micro.NewService(
|
||||
// micro.WrapHandler(auth.AuthHandler(auth.HandlerOptions{
|
||||
// Auth: myAuthProvider,
|
||||
// Rules: myRules,
|
||||
// SkipEndpoints: []string{"Health.Check"},
|
||||
// })),
|
||||
// )
|
||||
func AuthHandler(opts HandlerOptions) server.HandlerWrapper {
|
||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||
// Get endpoint name
|
||||
endpoint := req.Endpoint()
|
||||
|
||||
// Check if this endpoint should skip auth
|
||||
for _, skip := range opts.SkipEndpoints {
|
||||
if skip == endpoint {
|
||||
// Skip auth, proceed to handler
|
||||
return h(ctx, req, rsp)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract metadata from context
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
return errors.Unauthorized(req.Service(), "missing metadata")
|
||||
}
|
||||
|
||||
// Extract and verify token
|
||||
token, err := TokenFromMetadata(md)
|
||||
if err != nil {
|
||||
if err == ErrMissingToken {
|
||||
return errors.Unauthorized(req.Service(), "missing authorization token")
|
||||
}
|
||||
return errors.Unauthorized(req.Service(), "invalid authorization token: %v", err)
|
||||
}
|
||||
|
||||
// Verify token and get account
|
||||
var account *auth.Account
|
||||
if opts.Auth != nil {
|
||||
account, err = opts.Auth.Inspect(token)
|
||||
if err != nil {
|
||||
if err == auth.ErrInvalidToken {
|
||||
return errors.Unauthorized(req.Service(), "invalid token")
|
||||
}
|
||||
return errors.Unauthorized(req.Service(), "token verification failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check authorization if rules are provided
|
||||
if opts.Rules != nil && account != nil {
|
||||
resource := &auth.Resource{
|
||||
Name: req.Service(),
|
||||
Type: "service",
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
|
||||
if err := opts.Rules.Verify(account, resource); err != nil {
|
||||
if err == auth.ErrForbidden {
|
||||
return errors.Forbidden(req.Service(), "access denied to %s", endpoint)
|
||||
}
|
||||
return errors.Forbidden(req.Service(), "authorization failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add account to context for handler to use
|
||||
if account != nil {
|
||||
ctx = auth.ContextWithAccount(ctx, account)
|
||||
}
|
||||
|
||||
// Call the handler
|
||||
return h(ctx, req, rsp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PublicEndpoints is a helper to create auth options that allow public access to specific endpoints.
|
||||
func PublicEndpoints(authProvider auth.Auth, rules auth.Rules, publicEndpoints []string) HandlerOptions {
|
||||
return HandlerOptions{
|
||||
Auth: authProvider,
|
||||
Rules: rules,
|
||||
SkipEndpoints: publicEndpoints,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRequired creates auth options that require authentication for all endpoints.
|
||||
func AuthRequired(authProvider auth.Auth, rules auth.Rules) HandlerOptions {
|
||||
return HandlerOptions{
|
||||
Auth: authProvider,
|
||||
Rules: rules,
|
||||
SkipEndpoints: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// AuthOptional creates auth options that extract auth if present but don't enforce it.
|
||||
// Useful for endpoints that behave differently for authenticated users but also work without auth.
|
||||
func AuthOptional(authProvider auth.Auth) server.HandlerWrapper {
|
||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||
// Try to extract account, but don't fail if missing
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if ok {
|
||||
if token, err := TokenFromMetadata(md); err == nil {
|
||||
if account, err := authProvider.Inspect(token); err == nil {
|
||||
ctx = auth.ContextWithAccount(ctx, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always call handler, with or without account in context
|
||||
return h(ctx, req, rsp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user