Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions context/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved
// Author: Prabhjot Singh Sethi <prabhjot.sethi@gmail.com>

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)
}
38 changes: 38 additions & 0 deletions context/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved
// Author: Prabhjot Singh Sethi <prabhjot.sethi@gmail.com>

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)
}
}
20 changes: 20 additions & 0 deletions context/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved
// Author: Prabhjot Singh Sethi <prabhjot.sethi@gmail.com>

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"
)
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=