From 4b601948977bd9a85774971035712bbec9ee257a Mon Sep 17 00:00:00 2001 From: royalcat Date: Wed, 25 Dec 2024 17:20:49 +0300 Subject: [PATCH] external login --- cmd/main.go | 19 ++- go.mod | 13 +- go.sum | 29 +++++ src/auth/auth.go | 120 ++++++++++++++++++ src/auth/ctx.go | 15 +++ src/auth/grpc.go | 66 ++++++++++ src/konfa/channel.go | 4 +- src/konfa/server.go | 9 -- src/konfa/user.go | 26 ++++ src/proto/auth.go | 97 -------------- src/proto/chat.go | 3 +- src/proto/servers.go | 2 +- src/store/models_user.go | 25 ++++ .../pg_migrations/1_initialize_schema.sql | 20 ++- src/store/pg_migrations/2_external_logins.sql | 11 ++ 15 files changed, 333 insertions(+), 126 deletions(-) create mode 100644 src/auth/auth.go create mode 100644 src/auth/ctx.go create mode 100644 src/auth/grpc.go create mode 100644 src/konfa/user.go delete mode 100644 src/proto/auth.go create mode 100644 src/store/models_user.go create mode 100644 src/store/pg_migrations/2_external_logins.sql diff --git a/cmd/main.go b/cmd/main.go index 5eb429c..ed19a2a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,6 +8,7 @@ import ( "github.com/ilyakaznacheev/cleanenv" "github.com/royalcat/konfa-server/pkg/uuid" + "github.com/royalcat/konfa-server/src/auth" "github.com/royalcat/konfa-server/src/konfa" "github.com/royalcat/konfa-server/src/proto" chatv1 "github.com/royalcat/konfa-server/src/proto/konfa/chat/v1" @@ -36,7 +37,19 @@ func main() { srv := konfa.NewService(db, dbpool) - grpcServer := grpc.NewServer(grpc.UnaryInterceptor(proto.Authenticate)) + authen, err := auth.NewAuthenticator(ctx, db, auth.AuthenticatorConfig{ + Issuer: "https://sso.konfach.ru/realms/konfach", + ClientID: "konfa", + ClientSecret: "UqeaMowRXcGULkAepr0EAEUfE82OjY72", + }) + if err != nil { + panic(err) + } + + grpcServer := grpc.NewServer( + grpc.UnaryInterceptor(authen.UnaryAuthenticate), + grpc.StreamInterceptor(authen.StreamAuthenticate), + ) chatv1.RegisterChatServiceServer(grpcServer, proto.NewChatService(srv)) serverv1.RegisterServerServiceServer(grpcServer, proto.NewServerService(srv)) @@ -80,7 +93,7 @@ func createKonfach(ctx context.Context, srv *konfa.Service) (uuid.UUID, uuid.UUI var chanID uuid.UUID - channels, err := srv.ListChannelsOnServer(ctx, serverID) + channels, err := srv.ListTextChannelsOnServer(ctx, serverID) if err != nil { return uuid.Nil, uuid.Nil, err } @@ -90,7 +103,7 @@ func createKonfach(ctx context.Context, srv *konfa.Service) (uuid.UUID, uuid.UUI } } if chanID == uuid.Nil { - chanID, err = srv.CreateChannel(ctx, serverID, "general") + chanID, err = srv.CreateTextChannel(ctx, serverID, "general") if err != nil { return uuid.Nil, uuid.Nil, fmt.Errorf("failed to create channel: %w", err) } diff --git a/go.mod b/go.mod index e620623..124dada 100644 --- a/go.mod +++ b/go.mod @@ -24,9 +24,12 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/coreos/go-oidc/v3 v3.11.0 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -42,13 +45,19 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mfridman/interpolate v0.0.2 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect + github.com/zitadel/logging v0.6.1 // indirect + github.com/zitadel/oidc v1.13.5 // indirect + github.com/zitadel/oidc/v3 v3.33.1 // indirect + github.com/zitadel/schema v1.3.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect @@ -56,10 +65,12 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/text v0.20.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 1f643cb..c9aa85a 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/cskr/pubsub/v2 v2.0.2 h1:395hhPXEsyI1b+5nfj+s5Q3gdxpg0jsWd3t/QAdmU1Y= github.com/cskr/pubsub/v2 v2.0.2/go.mod h1:XYuiN8dhcXTCzQDa5SH4+B3zLso94FTwAk0maAEGJJw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,6 +33,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -48,6 +54,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -97,6 +105,8 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -117,6 +127,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -141,6 +153,14 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI= github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= +github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= +github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= +github.com/zitadel/oidc v1.13.5 h1:7jhh68NGZitLqwLiVU9Dtwa4IraJPFF1vS+4UupO93U= +github.com/zitadel/oidc v1.13.5/go.mod h1:rHs1DhU3Sv3tnI6bQRVlFa3u0lCwtR7S21WHY+yXgPA= +github.com/zitadel/oidc/v3 v3.33.1 h1:e3w9PDV0Mh50/ZiJWtzyT0E4uxJ6RXll+hqVDnqGbTU= +github.com/zitadel/oidc/v3 v3.33.1/go.mod h1:zkoZ1Oq6CweX3BaLrftLEGCs6YK6zDpjjVGZrP10AWU= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= @@ -159,14 +179,21 @@ golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= @@ -176,6 +203,8 @@ google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/auth/auth.go b/src/auth/auth.go new file mode 100644 index 0000000..f477918 --- /dev/null +++ b/src/auth/auth.go @@ -0,0 +1,120 @@ +package auth + +import ( + "context" + "database/sql" + "errors" + + "github.com/royalcat/konfa-server/pkg/uuid" + "github.com/royalcat/konfa-server/src/store" + "github.com/uptrace/bun" + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata") + errInvalidToken = status.Errorf(codes.Unauthenticated, "invalid token") +) + +type AuthenticatorConfig struct { + Issuer string + ClientID string + ClientSecret string +} + +type Authenticator struct { + provider rs.ResourceServer + db *bun.DB +} + +func NewAuthenticator(ctx context.Context, db *bun.DB, acfg AuthenticatorConfig) (*Authenticator, error) { + + provider, err := rs.NewResourceServerClientCredentials(ctx, acfg.Issuer, acfg.ClientID, acfg.ClientSecret) + if err != nil { + return nil, err + } + + return &Authenticator{ + provider: provider, + db: db, + }, nil +} + +func (a *Authenticator) authorize(ctx context.Context, token string) (context.Context, error) { + // var claims oidc.AccessTokenClaims + // _, err := oidc.ParseToken(token, &claims) + // if err != nil { + // return nil, err + // } + + resp, err := rs.Introspect[*oidc.IntrospectionResponse](ctx, a.provider, token) + if err != nil { + return nil, err + } + + user, err := a.loginWithExternal(ctx, resp) + if err != nil { + return nil, err + } + + ctx = context.WithValue(ctx, ctxUserKey, &user) + + return ctx, nil +} + +func (a *Authenticator) loginWithExternal(ctx context.Context, resp *oidc.IntrospectionResponse) (store.User, error) { + var user store.User + err := a.db.NewSelect(). + Model(&user). + Join("JOIN external_login ON \"user\".\"id\" = \"external_login\".\"user_id\""). + Where("issuer = ?", resp.Issuer). + Where("subject = ?", resp.Subject). + Scan(ctx) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return a.createUserFromExternal(ctx, resp) + } + + return store.User{}, err + } + + return user, nil +} + +func (a *Authenticator) createUserFromExternal(ctx context.Context, resp *oidc.IntrospectionResponse) (store.User, error) { + user := store.User{ + ID: uuid.New(), + Username: resp.Username, + } + + err := a.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + _, err := tx.NewInsert(). + Model(&user). + Exec(ctx) + if err != nil { + return err + } + + _, err = tx.NewInsert(). + Model(&store.ExternalLogin{ + UserID: user.ID, + Issuer: resp.Issuer, + Subject: resp.Subject, + }). + Exec(ctx) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return store.User{}, err + } + + return user, nil +} diff --git a/src/auth/ctx.go b/src/auth/ctx.go new file mode 100644 index 0000000..0ca921a --- /dev/null +++ b/src/auth/ctx.go @@ -0,0 +1,15 @@ +package auth + +import ( + "context" + + "github.com/royalcat/konfa-server/src/store" +) + +type ctxKey string + +const ctxUserKey ctxKey = "user" + +func CtxGetUser(ctx context.Context) *store.User { + return ctx.Value(ctxUserKey).(*store.User) +} diff --git a/src/auth/grpc.go b/src/auth/grpc.go new file mode 100644 index 0000000..f83863a --- /dev/null +++ b/src/auth/grpc.go @@ -0,0 +1,66 @@ +package auth + +import ( + "context" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func (a *Authenticator) UnaryAuthenticate(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, errMissingMetadata + } + + token := grpcExtractToken(md["authorization"]) + if token == "" { + return nil, errInvalidToken + } + + ctx, err := a.authorize(ctx, token) + if err != nil { + return nil, err + } + + return handler(ctx, req) +} + +func (a *Authenticator) StreamAuthenticate(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx := ss.Context() + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return errMissingMetadata + } + + token := grpcExtractToken(md["authorization"]) + if token == "" { + return errInvalidToken + } + + ctx, err := a.authorize(ctx, token) + if err != nil { + return err + } + + return handler(srv, newWrappedStream(ctx, ss)) +} + +func grpcExtractToken(authorization []string) string { + if len(authorization) < 1 { + return "" + } + + return strings.TrimPrefix(authorization[0], "Bearer ") +} + +type wrappedStreamContext struct { + ctx context.Context + grpc.ServerStream +} + +func newWrappedStream(ctx context.Context, s grpc.ServerStream) grpc.ServerStream { + return &wrappedStreamContext{ctx: ctx, ServerStream: s} +} diff --git a/src/konfa/channel.go b/src/konfa/channel.go index 7bf43e8..1c44aa0 100644 --- a/src/konfa/channel.go +++ b/src/konfa/channel.go @@ -7,7 +7,7 @@ import ( "github.com/royalcat/konfa-server/src/store" ) -func (c *Service) CreateChannel(ctx context.Context, serverID uuid.UUID, name string) (uuid.UUID, error) { +func (c *Service) CreateTextChannel(ctx context.Context, serverID uuid.UUID, name string) (uuid.UUID, error) { channel := store.TextChannel{ ID: uuid.New(), ServerID: serverID, @@ -34,7 +34,7 @@ func (c *Service) GetChannel(ctx context.Context, serverID uuid.UUID, channelID return channel, err } -func (c *Service) ListChannelsOnServer(ctx context.Context, serverID uuid.UUID) ([]store.TextChannel, error) { +func (c *Service) ListTextChannelsOnServer(ctx context.Context, serverID uuid.UUID) ([]store.TextChannel, error) { var channels []store.TextChannel err := c.db.NewSelect(). Model(&channels). diff --git a/src/konfa/server.go b/src/konfa/server.go index a621a6e..9cbf303 100644 --- a/src/konfa/server.go +++ b/src/konfa/server.go @@ -34,12 +34,3 @@ func (c *Service) ListServers(ctx context.Context) ([]store.Server, error) { Scan(ctx) return servers, err } - -func (c *Service) ListTextChannels(ctx context.Context, serverID uuid.UUID) ([]store.TextChannel, error) { - var channels []store.TextChannel - err := c.db.NewSelect(). - Model(&channels). - Where("server_id = ?", serverID). - Scan(ctx) - return channels, err -} diff --git a/src/konfa/user.go b/src/konfa/user.go new file mode 100644 index 0000000..bac9de9 --- /dev/null +++ b/src/konfa/user.go @@ -0,0 +1,26 @@ +package konfa + +import ( + "context" + + "github.com/royalcat/konfa-server/pkg/uuid" + "github.com/royalcat/konfa-server/src/store" + "github.com/uptrace/bun" +) + +type Users struct { + db *bun.DB +} + +func NewUsers(db *bun.DB) *Users { + return &Users{db: db} +} + +func (u *Users) GetUser(ctx context.Context, id uuid.UUID) (store.User, error) { + var user store.User + err := u.db.NewSelect(). + Model(&user). + Where("id = ?", id). + Scan(ctx) + return user, err +} diff --git a/src/proto/auth.go b/src/proto/auth.go deleted file mode 100644 index 5ee2b3f..0000000 --- a/src/proto/auth.go +++ /dev/null @@ -1,97 +0,0 @@ -package proto - -import ( - "context" - "flag" - "strings" - - "github.com/royalcat/konfa-server/pkg/uuid" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -var ( - errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata") - errInvalidToken = status.Errorf(codes.Unauthenticated, "invalid token") -) - -var port = flag.Int("port", 50051, "the port to serve on") - -// func main() { -// flag.Parse() -// fmt.Printf("server starting on port %d...\n", *port) - -// cert, err := tls.LoadX509KeyPair(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem")) -// if err != nil { -// log.Fatalf("failed to load key pair: %s", err) -// } -// opts := []grpc.ServerOption{ -// // The following grpc.ServerOption adds an interceptor for all unary -// // RPCs. To configure an interceptor for streaming RPCs, see: -// // https://godoc.org/google.golang.org/grpc#StreamInterceptor -// grpc.UnaryInterceptor(ensureValidToken), -// // Enable TLS for all incoming connections. -// grpc.Creds(credentials.NewServerTLSFromCert(&cert)), -// } -// s := grpc.NewServer(opts...) -// pb.RegisterEchoServer(s, &ecServer{}) -// lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) -// if err != nil { -// log.Fatalf("failed to listen: %v", err) -// } -// if err := s.Serve(lis); err != nil { -// log.Fatalf("failed to serve: %v", err) -// } -// } - -// valid validates the authorization. -func valid(authorization []string) bool { - if len(authorization) < 1 { - return false - } - token := strings.TrimPrefix(authorization[0], "Bearer ") - // Perform the token validation here. For the sake of this example, the code - // here forgoes any of the usual OAuth2 token validation and instead checks - // for a token matching an arbitrary string. - return token == "some-secret-token" -} - -type ctxKey string - -const ctxUserKey ctxKey = "user" - -// Authenticate ensures a valid token exists within a request's metadata. If -// the token is missing or invalid, the interceptor blocks execution of the -// handler and returns an error. Otherwise, the interceptor invokes the unary -// handler. -func Authenticate(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - // md, ok := metadata.FromIncomingContext(ctx) - // if !ok { - // return nil, errMissingMetadata - // } - // // The keys within metadata.MD are normalized to lowercase. - // // See: https://godoc.org/google.golang.org/grpc/metadata#New - // if !valid(md["authorization"]) { - // return nil, errInvalidToken - // } - - var user User - - user = User{ - ID: uuid.MustFromString("a903b474-26f4-4262-9ba7-97edaa76491f"), - } - - ctx = context.WithValue(ctx, ctxUserKey, &user) - - // Continue execution of handler after ensuring a valid token. - return handler(ctx, req) -} - -type User struct { - ID uuid.UUID -} - -func getCtxUser(ctx context.Context) *User { - return ctx.Value(ctxUserKey).(*User) -} diff --git a/src/proto/chat.go b/src/proto/chat.go index 6c99a14..5f77de6 100644 --- a/src/proto/chat.go +++ b/src/proto/chat.go @@ -4,6 +4,7 @@ import ( "context" "github.com/royalcat/konfa-server/pkg/uuid" + "github.com/royalcat/konfa-server/src/auth" "github.com/royalcat/konfa-server/src/konfa" chatv1 "github.com/royalcat/konfa-server/src/proto/konfa/chat/v1" "google.golang.org/grpc" @@ -27,7 +28,7 @@ var _ chatv1.ChatServiceServer = (*ChatService)(nil) // SendMessage implements chatv1.ChatServiceServer. func (c *ChatService) SendMessage(ctx context.Context, req *chatv1.SendMessageRequest) (*chatv1.SendMessageResponse, error) { - user := getCtxUser(ctx) + user := auth.CtxGetUser(ctx) if user == nil { return nil, ErrUnauthenticated } diff --git a/src/proto/servers.go b/src/proto/servers.go index cb05530..405ab4d 100644 --- a/src/proto/servers.go +++ b/src/proto/servers.go @@ -27,7 +27,7 @@ func (s *ServerService) ListServerChannels(ctx context.Context, req *serverv1.Li return nil, err } - textChannels, err := s.srv.ListTextChannels(ctx, serverID) + textChannels, err := s.srv.ListTextChannelsOnServer(ctx, serverID) if err != nil { return nil, err } diff --git a/src/store/models_user.go b/src/store/models_user.go new file mode 100644 index 0000000..b5ea854 --- /dev/null +++ b/src/store/models_user.go @@ -0,0 +1,25 @@ +package store + +import ( + "time" + + "github.com/royalcat/konfa-server/pkg/uuid" + "github.com/uptrace/bun" +) + +type User struct { + bun.BaseModel `bun:"table:user"` + + ID uuid.UUID `bun:"id,pk"` + Username string `bun:"username"` +} + +type ExternalLogin struct { + bun.BaseModel `bun:"table:external_login"` + + ID uuid.UUID `bun:"id,pk"` + UserID uuid.UUID `bun:"user_id"` + Issuer string `bun:"issuer"` + Subject string `bun:"subject"` + CreatedAt time.Time `bun:"created_at"` +} diff --git a/src/store/pg_migrations/1_initialize_schema.sql b/src/store/pg_migrations/1_initialize_schema.sql index 92827ea..523d571 100644 --- a/src/store/pg_migrations/1_initialize_schema.sql +++ b/src/store/pg_migrations/1_initialize_schema.sql @@ -3,32 +3,28 @@ -- User Table CREATE TABLE "user" ( id uuid PRIMARY KEY, - username VARCHAR(255) UNIQUE + username VARCHAR(255) NOT NULL UNIQUE ); -- Server Table CREATE TABLE "server" (id uuid PRIMARY KEY, "name" VARCHAR(255)); -- TextChannel Table CREATE TABLE "text_channel" ( - id uuid PRIMARY KEY, - server_id uuid, - FOREIGN KEY (server_id) REFERENCES "server"(id), - "name" VARCHAR(255) + id UUID PRIMARY KEY, + server_id UUID NOT NULL REFERENCES "server"(id) ON DELETE CASCADE, + "name" VARCHAR(255) NOT NULL ); CREATE UNIQUE INDEX unique_text_channel_name ON "text_channel"(server_id, "name"); -- VoiceChannel Table CREATE TABLE "voice_channel" ( id uuid PRIMARY KEY, - server_id uuid, - FOREIGN KEY (server_id) REFERENCES "server"(id), - "name" VARCHAR(255) + server_id uuid NOT NULL REFERENCES "server"(id) ON DELETE CASCADE, + "name" VARCHAR(255) NOT NULL ); -- Message Table CREATE TABLE "message" ( id uuid PRIMARY KEY, - channel_id uuid NOT NULL, - FOREIGN KEY (channel_id) REFERENCES "text_channel"(id), - sender_id uuid NOT NULL, - FOREIGN KEY (sender_id) REFERENCES "user"(id), + channel_id uuid NOT NULL REFERENCES "text_channel"(id) ON DELETE CASCADE, + sender_id uuid NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, content TEXT NOT NULL, "timestamp" TIMESTAMPTZ NOT NULL DEFAULT now() ); diff --git a/src/store/pg_migrations/2_external_logins.sql b/src/store/pg_migrations/2_external_logins.sql new file mode 100644 index 0000000..bc12fa1 --- /dev/null +++ b/src/store/pg_migrations/2_external_logins.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE "external_login" ( + "id" uuid PRIMARY KEY, + "user_id" uuid NOT NULL REFERENCES "user" (id) ON DELETE CASCADE, + "issuer" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE UNIQUE INDEX external_login_unique_external_id ON "external_login" ("issuer", "subject"); +-- +goose StatementEnd \ No newline at end of file