From a1489adc6cc8ed183348b635c26d20d96dac8bb4 Mon Sep 17 00:00:00 2001 From: Prabhjot Singh Sethi Date: Mon, 2 Jun 2025 10:02:12 +0000 Subject: [PATCH] Move the auth context code from core to auth repo Signed-off-by: Prabhjot Singh Sethi --- context/auth.go | 123 +++++++++++++++++++++++++++++++++++++++++++ context/auth_test.go | 38 +++++++++++++ context/const.go | 20 +++++++ go.mod | 7 +++ go.sum | 6 +++ 5 files changed, 194 insertions(+) create mode 100644 context/auth.go create mode 100644 context/auth_test.go create mode 100644 context/const.go create mode 100644 go.sum diff --git a/context/auth.go b/context/auth.go new file mode 100644 index 0000000..6eb5586 --- /dev/null +++ b/context/auth.go @@ -0,0 +1,123 @@ +// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved +// Author: Prabhjot Singh Sethi + +package context + +import ( + "context" + "encoding/base64" + "encoding/json" + "net/http" + + "google.golang.org/grpc/metadata" + + "github.com/go-core-stack/core/errors" +) + +// Auth construct obtained as part of the auth action being performed +// while processing a request, this is json tagged to allow passing +// the inforamtion internally in the system between the microservices +// we can validate entities like user, devices, service accounts etc +type AuthInfo struct { + Realm string `json:"realm,omitempty"` + UserName string `json:"preferred_username"` + Email string `json:"email,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` + FullName string `json:"name,omitempty"` + FirstName string `json:"given_name,omitempty"` + LastName string `json:"family_name,omitempty"` + SessionID string `json:"sid,omitempty"` +} + +// struct identifier for the context +type authInfo struct{} + +// Sets Auth Info Header in the provided Http Request typically will +// be used only by the entity that has performed that authentication +// on the given http request already and has the relevant Auth Info +// Context. +func SetAuthInfoHeader(r *http.Request, info *AuthInfo) error { + b, err := json.Marshal(info) + if err != nil { + return errors.Wrapf(errors.InvalidArgument, "failed to generate user info: %s", err) + } + val := base64.RawURLEncoding.EncodeToString(b) + r.Header.Set(HttpClientAuthContext, val) + return nil +} + +// gets Auth Info Header available in the Http Request +func GetAuthInfoHeader(r *http.Request) (*AuthInfo, error) { + val := r.Header.Get(HttpClientAuthContext) + if val == "" { + return nil, errors.Wrapf(errors.NotFound, "Auth info not available in the http request") + } + b, err := base64.RawURLEncoding.DecodeString(val) + if err != nil { + return nil, errors.Wrapf(errors.InvalidArgument, "invalid user info received: %s", err) + } + info := &AuthInfo{} + err = json.Unmarshal(b, info) + if err != nil { + return nil, errors.Wrapf(errors.InvalidArgument, "failed to get user info from header: %s", err) + } + return info, nil +} + +// extract the header information from the GRPC context +func extractHeader(ctx context.Context, header string) (string, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", errors.Wrapf(errors.NotFound, "No Metadata available in incoming message") + } + + hValue, ok := md[header] + if !ok { + return "", errors.Wrapf(errors.NotFound, "missing header: %s", header) + } + + if len(hValue) != 1 { + return "", errors.Wrapf(errors.NotFound, "no value associated with header: %s", header) + } + + return hValue[0], nil +} + +// Processes the headers available in context, to validate that the authentication is already performed +func ProcessAuthInfo(ctx context.Context) (context.Context, error) { + val, err := extractHeader(ctx, GrpcClientAuthContext) + if err != nil { + return ctx, errors.Wrapf(errors.Unauthorized, "failed to extract auth info header: %s", err) + } + + b, err := base64.RawURLEncoding.DecodeString(val) + if err != nil { + return ctx, errors.Wrapf(errors.Unauthorized, "invalid user info received: %s", err) + } + + info := &AuthInfo{} + err = json.Unmarshal(b, info) + if err != nil { + return ctx, errors.Wrapf(errors.Unauthorized, "failed to get user info from header: %s", err) + } + + // create new context with value of the auth info + authCtx := context.WithValue(ctx, authInfo{}, info) + return authCtx, nil +} + +// gets Auth Info from Context available in the Http Request +func GetAuthInfoFromContext(ctx context.Context) (*AuthInfo, error) { + val := ctx.Value(authInfo{}) + switch info := val.(type) { + case *AuthInfo: + return info, nil + default: + return nil, errors.Wrapf(errors.NotFound, "auth info not found") + } +} + +// delete the Auth info header from the given HTTP request +func DeleteAuthInfoHeader(r *http.Request) { + r.Header.Del(HttpClientAuthContext) +} diff --git a/context/auth_test.go b/context/auth_test.go new file mode 100644 index 0000000..9ca8018 --- /dev/null +++ b/context/auth_test.go @@ -0,0 +1,38 @@ +// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved +// Author: Prabhjot Singh Sethi + +package context + +import ( + "fmt" + "net/http" + "testing" +) + +func Test_ErrorValidations(t *testing.T) { + r := &http.Request{ + Header: http.Header{}, + } + info := &AuthInfo{ + Realm: "root", + UserName: "admin", + Email: "admin@example.com", + FullName: "Test Admin", + SessionID: "abc", + } + _ = SetAuthInfoHeader(r, info) + fmt.Printf("Got - Encoded Auth Info: %s\n", r.Header[HttpClientAuthContext][0]) + if r.Header[HttpClientAuthContext][0] != "eyJyZWFsbSI6InJvb3QiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJuYW1lIjoiVGVzdCBBZG1pbiIsInNpZCI6ImFiYyJ9" { + t.Errorf("failed to set the auth info in the header, found invalid value in header") + } + found, err := GetAuthInfoHeader(r) + if err != nil { + t.Errorf("got error while getting auth info: %s", err) + } + if found.Realm != info.Realm { + t.Errorf("expected realm to be %s, but got %s", info.Realm, found.Realm) + } + if found.UserName != info.UserName { + t.Errorf("expected UserName to be %s, but got %s", info.UserName, found.UserName) + } +} diff --git a/context/const.go b/context/const.go new file mode 100644 index 0000000..e9fc893 --- /dev/null +++ b/context/const.go @@ -0,0 +1,20 @@ +// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved +// Author: Prabhjot Singh Sethi + +package context + +const ( + // Internal Auth Context Header, carries information of the + // client that has been authenticated. + // Where content itself will be of usual string format, which + // is obtained by json marshaling of struct AuthInfo followed + // by base64 encoding of the json marshaled content. + // + // This is usually Added by Auth Gateway, if present it + // indicates that authentication is successfully performed + // by Auth Gateway. + HttpClientAuthContext = "Auth-Info" + + // grpc gateway will typically move the header to lowercase + GrpcClientAuthContext = "auth-info" +) diff --git a/go.mod b/go.mod index 662ae1b..6ddf1ae 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/go-core-stack/auth go 1.24 + +require google.golang.org/grpc v1.72.2 + +require ( + github.com/go-core-stack/core v0.0.0-20250602095754-4e9ba9991c48 + golang.org/x/sys v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fac7e68 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/go-core-stack/core v0.0.0-20250602095754-4e9ba9991c48 h1:Zx6wayr1O3+SEsj1FVbFSwhquFwj9lVwcnfg4WBrKHQ= +github.com/go-core-stack/core v0.0.0-20250602095754-4e9ba9991c48/go.mod h1:x2FDn7vxfDMb/V4PIJ8Fr1+nz8yMkBPZgF/16cSZVS0= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=