diff --git a/v2/authorization_claims.go b/v2/authorization_claims.go index f1eccc5..de4b706 100644 --- a/v2/authorization_claims.go +++ b/v2/authorization_claims.go @@ -23,10 +23,13 @@ import ( // ServerID is basic static info for a NATS server. type ServerID struct { - Name string `json:"name"` - Host string `json:"host"` - ID string `json:"id"` - XKey string `json:"xkey,omitempty"` + Name string `json:"name"` + Host string `json:"host"` + ID string `json:"id"` + Version string `json:"version,omitempty"` + Cluster string `json:"cluster,omitempty"` + Tags TagList `json:"tags,omitempty"` + XKey string `json:"xkey,omitempty"` } // ClientInformation is information about a client that is trying to authorize. @@ -135,7 +138,7 @@ func (ac *AuthorizationRequestClaims) ClaimType() ClaimType { return ac.Type } -// Claims returns the accounts claims data. +// Claims returns the request claims data. func (ac *AuthorizationRequestClaims) Claims() *ClaimsData { return &ac.ClaimsData } @@ -152,3 +155,101 @@ func (ac *AuthorizationRequestClaims) String() string { func (ac *AuthorizationRequestClaims) updateVersion() { ac.GenericFields.Version = libVersion } + +// Represents an authorization response error. +type AuthorizationError struct { + Description string `json:"description"` +} + +// AuthorizationResponse represents a response to an authorization callout. +// Will be a valid user or an error. +type AuthorizationResponse struct { + User *UserClaims `json:"user_claims,omitempty"` + Error *AuthorizationError `json:"error,omitempty"` + GenericFields +} + +// AuthorizationResponseClaims defines an external auth response. +// This will be signed by the trusted account issuer. +// Will contain a valid user JWT or an error. +// These wil be signed by a NATS server. +type AuthorizationResponseClaims struct { + ClaimsData + AuthorizationResponse `json:"nats"` +} + +// Create a new response claim for the given subject. +func NewAuthorizationResponseClaims(subject string) *AuthorizationResponseClaims { + if subject == "" { + return nil + } + var arc AuthorizationResponseClaims + arc.Subject = subject + return &arc +} + +// Set's and error description. +func (arc *AuthorizationResponseClaims) SetErrorDescription(errDescription string) { + if arc.Error != nil { + arc.Error.Description = errDescription + } else { + arc.Error = &AuthorizationError{Description: errDescription} + } +} + +// Validate checks the generic and specific parts of the auth request jwt. +func (arc *AuthorizationResponseClaims) Validate(vr *ValidationResults) { + if arc.User == nil && arc.Error == nil { + vr.AddError("User or error required") + } + if arc.User != nil && arc.Error != nil { + vr.AddError("User and error can not both be set") + } + arc.ClaimsData.Validate(vr) +} + +// Encode tries to turn the auth request claims into a JWT string. +func (arc *AuthorizationResponseClaims) Encode(pair nkeys.KeyPair) (string, error) { + arc.Type = AuthorizationResponseClaim + return arc.ClaimsData.encode(pair, arc) +} + +// DecodeAuthorizationResponseClaims tries to parse an auth response claim from a JWT string +func DecodeAuthorizationResponseClaims(token string) (*AuthorizationResponseClaims, error) { + claims, err := Decode(token) + if err != nil { + return nil, err + } + arc, ok := claims.(*AuthorizationResponseClaims) + if !ok { + return nil, errors.New("not an authorization response claim") + } + return arc, nil +} + +// ExpectedPrefixes defines the types that can encode an auth response jwt which is accounts. +func (arc *AuthorizationResponseClaims) ExpectedPrefixes() []nkeys.PrefixByte { + return []nkeys.PrefixByte{nkeys.PrefixByteAccount} +} + +func (arc *AuthorizationResponseClaims) ClaimType() ClaimType { + return arc.Type +} + +// Claims returns the response claims data. +func (arc *AuthorizationResponseClaims) Claims() *ClaimsData { + return &arc.ClaimsData +} + +// Payload pulls the request specific payload out of the claims. +func (arc *AuthorizationResponseClaims) Payload() interface{} { + return &arc.AuthorizationResponse +} + +func (arc *AuthorizationResponseClaims) String() string { + return arc.ClaimsData.String(arc) +} + +func (arc *AuthorizationResponseClaims) updateVersion() { + arc.GenericFields.Version = libVersion +} diff --git a/v2/authorization_claims_test.go b/v2/authorization_claims_test.go index feb2746..8e48fc7 100644 --- a/v2/authorization_claims_test.go +++ b/v2/authorization_claims_test.go @@ -21,7 +21,7 @@ import ( "github.com/nats-io/nkeys" ) -func TestNewAuthorizationClaims(t *testing.T) { +func TestNewAuthorizationRequestClaims(t *testing.T) { skp, _ := nkeys.CreateServer() ac := NewAuthorizationRequestClaims("TEST") ac.Server.Name = "NATS-1" @@ -65,3 +65,67 @@ func TestNewAuthorizationClaims(t *testing.T) { AssertEquals(ac.String(), ac2.String(), t) AssertEquals(ac.Server.Name, ac2.Server.Name, t) } + +func TestNewAuthorizationResponseClaims(t *testing.T) { + // Make sure one or other is set. + var empty AuthorizationResponseClaims + vr := CreateValidationResults() + empty.Validate(vr) + if vr.IsEmpty() || !vr.IsBlocking(false) { + t.Fatalf("Expected blocking error on an empty authorization response") + } + + // Make sure both can not be set. + // Create user, account etc. + akp := createAccountNKey(t) + ukp := createUserNKey(t) + + uclaim := NewUserClaims(publicKey(ukp, t)) + uclaim.Audience = publicKey(akp, t) + + arc := NewAuthorizationResponseClaims("TEST") + arc.User = uclaim + arc.Error = &AuthorizationError{Description: "BAD"} + + vr = CreateValidationResults() + arc.Validate(vr) + if vr.IsEmpty() || !vr.IsBlocking(false) { + t.Fatalf("Expected blocking error when both user and error are set") + } + + // Clear error and make sure ok. + arc.Error = nil + // should be server public key. + skp := createServerNKey(t) + arc.Audience = publicKey(skp, t) + + vr = CreateValidationResults() + arc.Validate(vr) + if !vr.IsEmpty() { + t.Fatal("Valid authorization response will have no validation results") + } + + arcJWT := encode(arc, akp, t) + arc2, err := DecodeAuthorizationResponseClaims(arcJWT) + if err != nil { + t.Fatal("error decoding authorization response jwt", err) + } + AssertEquals(arc.String(), arc2.String(), t) + + // Check that error constructor works. + arc = NewAuthorizationResponseClaims("TEST") + arc.SetErrorDescription("BAD CERT") + + vr = CreateValidationResults() + arc.Validate(vr) + if !vr.IsEmpty() { + t.Fatal("Valid authorization response will have no validation results") + } + + arcJWT = encode(arc, akp, t) + arc2, err = DecodeAuthorizationResponseClaims(arcJWT) + if err != nil { + t.Fatal("error decoding authorization response jwt", err) + } + AssertEquals(arc.String(), arc2.String(), t) +} diff --git a/v2/claims.go b/v2/claims.go index a9d1a61..7410818 100644 --- a/v2/claims.go +++ b/v2/claims.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 The NATS Authors + * Copyright 2018-2022 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -40,7 +40,9 @@ const ( // ActivationClaim is the type of an activation JWT ActivationClaim = "activation" // AuthorizationRequestClaim is the type of an auth request claim JWT - AuthorizationRequestClaim = "authorization" + AuthorizationRequestClaim = "authorization_request" + // AuthorizationResponseClaim is the type of an auth response claim JWT + AuthorizationResponseClaim = "authorization_response" // GenericClaim is a type that doesn't match Operator/Account/User/ActionClaim GenericClaim = "generic" ) @@ -55,6 +57,8 @@ func IsGenericClaimType(s string) bool { fallthrough case AuthorizationRequestClaim: fallthrough + case AuthorizationResponseClaim: + fallthrough case ActivationClaim: return false case GenericClaim: diff --git a/v2/decoder.go b/v2/decoder.go index ff796ca..1e043a3 100644 --- a/v2/decoder.go +++ b/v2/decoder.go @@ -1,5 +1,5 @@ /* - * Copyright 2020 The NATS Authors + * Copyright 2020-2022 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -145,7 +145,9 @@ func loadClaims(data []byte) (int, Claims, error) { case ActivationClaim: claim, err = loadActivation(data, id.Version()) case AuthorizationRequestClaim: - claim, err = loadAuthorization(data, id.Version()) + claim, err = loadAuthorizationRequest(data, id.Version()) + case AuthorizationResponseClaim: + claim, err = loadAuthorizationResponse(data, id.Version()) case "cluster": return -1, nil, errors.New("ClusterClaims are not supported") case "server": diff --git a/v2/decoder_authorization.go b/v2/decoder_authorization.go index 7113073..f3cc2bd 100644 --- a/v2/decoder_authorization.go +++ b/v2/decoder_authorization.go @@ -19,10 +19,18 @@ import ( "encoding/json" ) -func loadAuthorization(data []byte, version int) (*AuthorizationRequestClaims, error) { +func loadAuthorizationRequest(data []byte, version int) (*AuthorizationRequestClaims, error) { var ac AuthorizationRequestClaims if err := json.Unmarshal(data, &ac); err != nil { return nil, err } return &ac, nil } + +func loadAuthorizationResponse(data []byte, version int) (*AuthorizationResponseClaims, error) { + var arc AuthorizationResponseClaims + if err := json.Unmarshal(data, &arc); err != nil { + return nil, err + } + return &arc, nil +}