Quando si proteggono le API, è importante comprendere due concetti distinti:
Goa fornisce costrutti DSL per definire sia i requisiti di autenticazione che di autorizzazione per i tuoi servizi.
JWT è uno standard aperto (RFC 7519) che definisce un modo compatto per trasmettere informazioni in modo sicuro tra le parti come oggetto JSON. I JWT sono spesso utilizzati sia per l’autenticazione che per l’autorizzazione:
var JWTAuth = JWTSecurity("jwt", func() {
Description("Autenticazione e autorizzazione basata su JWT")
// Gli scope definiscono i permessi che possono essere verificati contro i claims JWT
Scope("api:read", "Accesso in sola lettura")
Scope("api:write", "Accesso in lettura e scrittura")
})
Gli scope sono permessi nominati che rappresentano quali azioni un client può eseguire. Quando si utilizzano i JWT:
Le chiavi API sono semplici token di stringa che i client includono con le loro richieste. Sebbene comunemente chiamate “Autenticazione con Chiave API”, sono più accuratamente descritte come un meccanismo di autorizzazione:
var APIKeyAuth = APIKeySecurity("api_key", func() {
Description("Autorizzazione delle richieste basata su chiave API")
})
Usi comuni per le chiavi API:
L’Autenticazione Base è un semplice schema di autenticazione integrato nel protocollo HTTP:
var BasicAuth = BasicAuthSecurity("basic", func() {
Description("Autenticazione tramite nome utente/password")
// Gli scope qui definiscono i permessi che possono essere concessi dopo l'autenticazione riuscita
Scope("api:read", "Accesso in sola lettura")
})
OAuth2 è un framework di autorizzazione completo che supporta flussi multipli per diversi tipi di applicazioni. Separa:
var OAuth2Auth = OAuth2Security("oauth2", func() {
// Definisci gli endpoint del flusso OAuth2
AuthorizationCodeFlow(
"http://auth.example.com/authorize", // Dove richiedere l'autorizzazione
"http://auth.example.com/token", // Dove scambiare il codice per il token
"http://auth.example.com/refresh", // Dove aggiornare i token scaduti
)
// Definisci i permessi disponibili
Scope("api:read", "Accesso in sola lettura")
Scope("api:write", "Accesso in lettura e scrittura")
})
Gli schemi di sicurezza possono essere applicati a diversi livelli:
Proteggi metodi individuali con uno o più schemi:
Method("secure_endpoint", func() {
Security(JWTAuth, func() {
Scope("api:read")
})
Payload(func() {
TokenField(1, "token", String)
Required("token")
})
HTTP(func() {
GET("/secure")
Response(StatusOK)
})
})
Combina più schemi di sicurezza per una sicurezza potenziata:
Method("doubly_secure", func() {
Security(JWTAuth, APIKeyAuth, func() {
Scope("api:write")
})
Payload(func() {
TokenField(1, "token", String)
APIKeyField(2, "api_key", "key", String)
Required("token", "key")
})
HTTP(func() {
POST("/secure")
Param("key:k") // Chiave API nel parametro di query
Response(StatusOK)
})
})
Configura come le credenziali di sicurezza vengono trasmesse su HTTP:
Method("secure_endpoint", func() {
Security(JWTAuth)
Payload(func() {
TokenField(1, "token", String)
Required("token")
})
HTTP(func() {
GET("/secure")
Header("token:Authorization") // JWT nell'header Authorization
Response(StatusOK)
Response("unauthorized", StatusUnauthorized)
})
})
Configura la sicurezza per il trasporto gRPC:
Method("secure_endpoint", func() {
Security(JWTAuth, APIKeyAuth)
Payload(func() {
TokenField(1, "token", String)
APIKeyField(2, "api_key", "key", String)
Required("token", "key")
})
GRPC(func() {
Metadata(func() {
Attribute("token:authorization") // JWT nei metadati
Attribute("api_key:x-api-key") // Chiave API nei metadati
})
Response(CodeOK)
Response("unauthorized", CodeUnauthenticated)
})
})
Definisci gli errori relativi alla sicurezza in modo coerente:
Service("secure_service", func() {
Error("unauthorized", String, "Credenziali non valide")
Error("forbidden", String, "Scope non validi")
HTTP(func() {
Response("unauthorized", StatusUnauthorized)
Response("forbidden", StatusForbidden)
})
GRPC(func() {
Response("unauthorized", CodeUnauthenticated)
Response("forbidden", CodePermissionDenied)
})
})
Design dell’Autenticazione
Design dell’Autorizzazione
Suggerimenti Generali
Quando definisci schemi di sicurezza nel tuo design, Goa genera un’interfaccia
Auther
specifica per il tuo design che il tuo servizio deve implementare. Questa
interfaccia definisce metodi per ogni schema di sicurezza che hai specificato:
// Auther definisce i requisiti di sicurezza per il servizio.
type Auther interface {
// BasicAuth implementa la logica di autorizzazione per l'auth base.
BasicAuth(context.Context, string, string, *security.BasicScheme) (context.Context, error)
// JWTAuth implementa la logica di autorizzazione per i token JWT.
JWTAuth(context.Context, string, *security.JWTScheme) (context.Context, error)
// APIKeyAuth implementa la logica di autorizzazione per le chiavi API.
APIKeyAuth(context.Context, string, *security.APIKeyScheme) (context.Context, error)
// OAuth2Auth implementa la logica di autorizzazione per OAuth2.
OAuth2Auth(context.Context, string, *security.OAuth2Scheme) (context.Context, error)
}
Il tuo servizio deve implementare questi metodi per gestire la logica di autenticazione/autorizzazione. Ecco come implementare ciascuno:
// BasicAuth implementa la logica di autorizzazione per lo schema di sicurezza "basic".
func (s *svc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) {
if user != "goa" || pass != "rocks" {
return ctx, ErrUnauthorized
}
// Memorizza le info di auth nel contesto per uso successivo
ctx = contextWithAuthInfo(ctx, authInfo{
user: user,
})
return ctx, nil
}
// JWTAuth implementa la logica di autorizzazione per lo schema di sicurezza "jwt".
func (s *svc) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) {
claims := make(jwt.MapClaims)
// Analizza e valida il token JWT
_, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) {
return Key, nil
})
if err != nil {
return ctx, ErrInvalidToken
}
// Valida gli scope richiesti
if claims["scopes"] == nil {
return ctx, ErrInvalidTokenScopes
}
scopes, ok := claims["scopes"].([]any)
if !ok {
return ctx, ErrInvalidTokenScopes
}
scopesInToken := make([]string, len(scopes))
for _, scp := range scopes {
scopesInToken = append(scopesInToken, scp.(string))
}
if err := scheme.Validate(scopesInToken); err != nil {
return ctx, securedservice.InvalidScopes(err.Error())
}
// Memorizza i claims nel contesto
ctx = contextWithAuthInfo(ctx, authInfo{
claims: claims,
})
return ctx, nil
}
// APIKeyAuth implementa la logica di autorizzazione per il servizio "secured_service"
// per lo schema di sicurezza "api_key".
func (s *securedServicesrvc) APIKeyAuth(ctx context.Context, key string, scheme *security.APIKeyScheme) (context.Context, error) {
if key != "my_awesome_api_key" {
return ctx, ErrUnauthorized
}
ctx = contextWithAuthInfo(ctx, authInfo{
key: key,
})
return ctx, nil
}
Quando si implementa un endpoint di accesso che emette token:
// Signin crea un token JWT valido per l'autenticazione
func (s *svc) Signin(ctx context.Context, p *gensvc.SigninPayload) (*gensvc.Creds, error) {
// Crea token JWT con claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
"iat": time.Now().Unix(),
"scopes": []string{"api:read", "api:write"},
})
// Firma il token
t, err := token.SignedString(Key)
if err != nil {
return nil, err
}
return &gensvc.Creds{
JWT: t,
OauthToken: t,
APIKey: "my_awesome_api_key",
}, nil
}
Quando implementi schemi di sicurezza nel tuo servizio Goa, ecco come funziona il flusso di autenticazione e autorizzazione:
Per esempio, con schemi multipli:
// Wrapper di endpoint generato
func NewDoublySecureEndpoint(s Service, authJWTFn security.AuthJWTFunc, authAPIKeyFn security.AuthAPIKeyFunc) goa.Endpoint {
return func(ctx context.Context, req any) (any, error) {
p := req.(*DoublySecurePayload)
// Valida prima JWT
ctx, err = authJWTFn(ctx, p.Token, &sc)
if err == nil {
// Poi valida la chiave API
ctx, err = authAPIKeyFn(ctx, p.Key, &sc)
}
if err != nil {
return nil, err
}
// Chiama il metodo del servizio se entrambi i controlli auth passano
return s.DoublySecure(ctx, p)
}
}