Skip to content

Commit

Permalink
Merge pull request #186 from nats-io/auth-updates
Browse files Browse the repository at this point in the history
Authorization Response and Server ID additions
  • Loading branch information
derekcollison authored Dec 27, 2022
2 parents ff7baa9 + 146945d commit bdf40fa
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 11 deletions.
111 changes: 106 additions & 5 deletions v2/authorization_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
66 changes: 65 additions & 1 deletion v2/authorization_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
8 changes: 6 additions & 2 deletions v2/claims.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
)
Expand All @@ -55,6 +57,8 @@ func IsGenericClaimType(s string) bool {
fallthrough
case AuthorizationRequestClaim:
fallthrough
case AuthorizationResponseClaim:
fallthrough
case ActivationClaim:
return false
case GenericClaim:
Expand Down
6 changes: 4 additions & 2 deletions v2/decoder.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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":
Expand Down
10 changes: 9 additions & 1 deletion v2/decoder_authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit bdf40fa

Please sign in to comment.