working on auth

This commit is contained in:
Ankit Patial 2024-11-19 10:40:30 +05:30
parent a2739dbbcd
commit 5954ec2501
13 changed files with 284 additions and 38 deletions

View File

@ -5,4 +5,4 @@ GRAPH_URL = http://localhost:3009
VITE_GRAPH_URL = http://localhost:3009
DB_URL = postgresql://root:root@127.0.0.1/rano_dev?search_path=public&sslmode=disable
MAILER_TEMPLATES_DIR = mailer/templates
MAILER_FROM_ADDRESS = NoReply<no-reply@my-app.com>
MAILER_FROM_ADDRESS = NoReply<no-reply@my-app.com>

3
config/certs/auth Normal file
View File

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA3EQvTeaEjR5CMk2Ka6/tUl9NaPRpvRggeto+vmReWB4=
-----END PUBLIC KEY-----

3
config/certs/auth.pub Normal file
View File

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA3EQvTeaEjR5CMk2Ka6/tUl9NaPRpvRggeto+vmReWB4=
-----END PUBLIC KEY-----

View File

@ -38,7 +38,7 @@ type (
WebPort int `env:"WEB_PORT"`
WebURL string `env:"WEB_URL"`
GraphPort int `env:"GRAPH_PORT"`
GrapURL string `env:"GRAPH_URL"`
GraphURL string `env:"GRAPH_URL"`
DbURL string `env:"DB_URL"`
MailerTplDir string `env:"MAILER_TEMPLATES_DIR"`
MailerFrom string `env:"MAILER_FROM_ADDRESS"`

View File

@ -8,6 +8,9 @@ import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"reflect"
"strings"
"time"
"contrib.go.opencensus.io/integrations/ocsql"
@ -58,24 +61,31 @@ func Client() *ent.Client {
return cl
}
type entity interface {
GetID() (int64, error)
}
// A AuditHook is an example for audit-log hook.
func AuditHook(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (v ent.Value, err error) {
// start timer
start := time.Now()
defer func() {
saveAudit(ctx, m.Type(), m.Op(), v, err, time.Since(start))
}()
// do the operation
v, err = next.Mutate(ctx, m)
// audit log
var id int64
if v != nil {
ev := reflect.Indirect(reflect.ValueOf(v))
f := ev.FieldByName("ID")
if f.IsValid() {
id = f.Int()
}
}
saveAudit(ctx, m.Type(), m.Op(), id, err, time.Since(start))
return
})
}
func saveAudit(ctx context.Context, t string, op ent.Op, v ent.Value, err error, d time.Duration) {
func saveAudit(ctx context.Context, t string, op ent.Op, id int64, err error, d time.Duration) {
if t == "Audit" {
// skip Audit table operations
return
@ -95,15 +105,30 @@ func saveAudit(ctx context.Context, t string, op ent.Op, v ent.Value, err error,
entOp = "UpdateOne"
}
big.
reqID := ctx.Value("RequestID")
ip := ctx.Value("RequestIP")
ua := ctx.Value("RequestUA")
if en, ok := v.(entity); ok {
id, _ := en.GetID()
logger.Info("%s %s %s-%s(%d) ip=%s ua=%s t=%v error=%e", reqID, status, op.String(), t, id, ip, ua, d, err)
} else {
logger.Info("%s %s %s-%s ip=%s ua=%s t=%v error=%e", reqID, status, op.String(), t, ip, ua, d, err)
var sb strings.Builder
if reqID, ok := ctx.Value("RequestID").(string); ok {
sb.WriteString(reqID + " ")
}
if err != nil {
sb.WriteString("failed ")
}
sb.WriteString(fmt.Sprintf("%s:%s:%d ", entOp, t, id))
if ip, ok := ctx.Value("RequestIP").(string); ok {
sb.WriteString(fmt.Sprintf("ip=%s ", ip))
}
if ua, ok := ctx.Value("RequestUA").(string); ok {
sb.WriteString(fmt.Sprintf("ip=%s ", ua))
}
if err != nil {
sb.WriteString(fmt.Sprintf("error=%s ", err))
}
sb.WriteString(fmt.Sprintf("t=%s", d))
logger.Info(sb.String())
}

11
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/99designs/gqlgen v0.17.56
github.com/brianvoe/gofakeit/v7 v7.1.2
github.com/jackc/pgx/v5 v5.7.1
github.com/lestrrat-go/jwx/v2 v2.1.2
github.com/sqids/sqids-go v0.4.1
github.com/vektah/gqlparser/v2 v2.5.19
gitserver.in/patialtech/mux v0.3.1
@ -17,11 +18,13 @@ require (
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-openapi/inflect v0.21.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -31,8 +34,14 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/zclconf/go-cty v1.15.0 // indirect
@ -51,7 +60,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/go-playground/validator/v10 v10.22.1
github.com/golang-migrate/migrate/v4 v4.18.1
github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/lib/pq v1.10.9 // indirect

19
go.sum
View File

@ -42,6 +42,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0=
@ -80,6 +82,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
@ -134,6 +138,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc=
github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
@ -159,6 +175,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
@ -171,6 +189,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=

View File

@ -17,15 +17,10 @@ import (
"gitserver.in/patialtech/rano/graph/model"
"gitserver.in/patialtech/rano/util/crypto"
"gitserver.in/patialtech/rano/util/logger"
"gitserver.in/patialtech/rano/util/uid"
)
type (
SessionUser struct {
ID string
Email string
Name string
RoleID int
}
AuthUser = model.AuthUser
)
@ -37,7 +32,7 @@ var (
)
func CtxWithUser(ctx context.Context, u *AuthUser) context.Context {
return context.WithValue(ctx, config.AuthUserCtxKey, &SessionUser{
return context.WithValue(ctx, config.AuthUserCtxKey, &AuthUser{
ID: u.ID,
Email: u.Email,
Name: u.Name,
@ -45,8 +40,8 @@ func CtxWithUser(ctx context.Context, u *AuthUser) context.Context {
})
}
func CtxUser(ctx context.Context) *SessionUser {
u, _ := ctx.Value(config.AuthUserCtxKey).(*SessionUser)
func CtxUser(ctx context.Context) *AuthUser {
u, _ := ctx.Value(config.AuthUserCtxKey).(*AuthUser)
return u
}
@ -62,16 +57,35 @@ func NewSession(ctx context.Context, email, pwd string) (*AuthUser, error) {
// 30 day token life
until := time.Now().Add(time.Hour * 24 * 30).UTC()
// user IP
ip, _ := ctx.Value("RequestIP").(string)
// user Agent
ua, _ := ctx.Value("RequestUA").(string)
// create sesion entry in db
db.Client().UserSession.Create().
// create session entry in db
s, err := db.Client().UserSession.Create().
SetUserID(u.ID).
SetIssuedAt(time.Now().UTC()).
SetExpiresAt(until).
SetIP("").
SetUserAgent("")
SetIP(ip).
SetUserAgent(ua).
Save(ctx)
if err != nil {
logger.Error(err)
return nil, ErrUnexpected
}
sid, err := uid.Encode(
uint64(u.ID),
uint64(s.ID),
)
if err != nil {
logger.Error(err)
return nil, ErrUnexpected
}
return &AuthUser{
ID: sid,
Name: fullName(u.FirstName, *u.MiddleName, u.LastName),
}, nil
}

View File

@ -19,11 +19,11 @@ var (
// newTokenToVerifyEmail for a user for given duration
func newTokenToVerifyEmail(userID int64, d time.Duration) (string, error) {
expiresAt := time.Now().Add(d).UTC().UnixMilli()
return uid.Encode([]uint64{
return uid.Encode(
uint64(userID),
1, // identifies that its token to verify email
uint64(expiresAt),
})
)
}
// tokenToVerifyEmail will check for valid email token that is yet not expired

99
util/crypto/ed25519.go Normal file
View File

@ -0,0 +1,99 @@
package crypto
import (
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"errors"
)
// NewPersistedED25519 public & private keys
func NewPersistedED25519() ([]byte, []byte, error) {
pub, prv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
// marshal public key
pubB, err := marshalEdPublicKey(pub)
if err != nil {
return nil, nil, err
}
// marshal private key
prvB, err := marshalEdPrivateKey(prv)
if err != nil {
return nil, nil, err
}
// done
return pubB, prvB, nil
}
func marshalEdPrivateKey(key ed25519.PrivateKey) ([]byte, error) {
b, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, err
}
enc := pem.EncodeToMemory(
&pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
},
)
return enc, nil
}
func parseEdPrivateKey(d []byte) (ed25519.PrivateKey, error) {
block, _ := pem.Decode(d)
if block == nil {
return nil, errors.New("failed to parse PEM block containing the key")
}
b, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
switch pub := b.(type) {
case ed25519.PrivateKey:
return pub, nil
default:
return nil, errors.New("key type is not RSA")
}
}
func marshalEdPublicKey(key ed25519.PublicKey) ([]byte, error) {
b, err := x509.MarshalPKIXPublicKey(key)
if err != nil {
return nil, err
}
enc := pem.EncodeToMemory(
&pem.Block{
Type: "PUBLIC KEY",
Bytes: b,
},
)
return enc, nil
}
func parseEdPublicKey(d []byte) (ed25519.PublicKey, error) {
block, _ := pem.Decode(d)
if block == nil {
return nil, errors.New("failed to parse PEM block containing the key")
}
b, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
switch pub := b.(type) {
case ed25519.PublicKey:
return pub, nil
default:
return nil, errors.New("key type is not RSA")
}
}

View File

@ -0,0 +1,16 @@
package crypto
import (
"testing"
)
func TestGenerate(t *testing.T) {
public, private, err := NewPersistedED25519()
if err != nil {
t.Error(err)
} else {
t.Log(string(public))
t.Log(string(private))
}
}

58
util/jwt/jwt.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2024 Patial Tech. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jwt
import (
"crypto/ed25519"
"fmt"
"time"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
j "github.com/lestrrat-go/jwx/v2/jwt"
)
func Sign(key ed25519.PrivateKey, claims map[string]interface{}, issuer string, d time.Duration) (string, error) {
prv, err := jwk.FromRaw(key)
if err != nil {
return "", fmt.Errorf("failed to create JWK, %w", err)
}
builder := j.NewBuilder().
Issuer(issuer).
IssuedAt(time.Now()).
Expiration(time.Now().Add(d))
for k, v := range claims {
builder = builder.Claim(k, v)
}
token, err := builder.Build()
signed, err := j.Sign(token, j.WithKey(jwa.EdDSA, prv))
if err != nil {
return "", fmt.Errorf("failed to generate signed payload, %w", err)
}
return string(signed), nil
}
func Parse(key ed25519.PrivateKey, payload string) (j.Token, error) {
prv, err := jwk.FromRaw(key)
if err != nil {
return nil, fmt.Errorf("failed to create JWK, %w", err)
}
pub, err := jwk.PublicKeyOf(prv)
if err != nil {
return nil, fmt.Errorf("failed on jwk.FromRaw, %w", err)
}
token, err := j.Parse([]byte(payload), j.WithKey(jwa.EdDSA, pub))
if err != nil {
return nil, fmt.Errorf("failed on jwk.FromRaw, %w", err)
}
return token, nil
}

View File

@ -13,7 +13,7 @@ var opts sqids.Options = sqids.Options{
}
// Encode a slice of IDs into one unique ID
func Encode(ids []uint64) (string, error) {
func Encode(ids ...uint64) (string, error) {
s, err := sqids.New()
if err != nil {
return "", err