From 6505490446fe8285e3320c9ae54775c756e1aecd Mon Sep 17 00:00:00 2001 From: michael12312 Date: Mon, 14 Oct 2024 20:07:24 +0800 Subject: [PATCH] feat: use oidc --- go.mod | 16 ++- go.sum | 35 ++--- internal/rbac/user.go | 44 ++++-- main.go | 4 +- pkg/auth/authmiddleware/authmiddleware.go | 25 ++-- pkg/auth/authmiddleware/checkauth.go | 10 +- pkg/auth/localmethod/localmethod.go | 9 +- pkg/auth/oidcmethod/callback.go | 157 ++++++++++++++++++++++ pkg/auth/oidcmethod/cookie.go | 100 ++++++++++++++ pkg/auth/oidcmethod/oidcmethod.go | 136 +++++++++++++++++++ pkg/auth/samlmethod/samlmethod.go | 2 +- pkg/config/config.go | 9 ++ pkg/config/etcd.go | 15 +++ pkg/database/etcd/client.go | 1 + pkg/router/feedback.go | 4 +- pkg/router/router.go | 4 +- pkg/router/userinfo.go | 2 +- 17 files changed, 506 insertions(+), 67 deletions(-) create mode 100644 pkg/auth/oidcmethod/callback.go create mode 100644 pkg/auth/oidcmethod/cookie.go create mode 100644 pkg/auth/oidcmethod/oidcmethod.go diff --git a/go.mod b/go.mod index ca70047..fe6b0e0 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,18 @@ require ( github.com/aws/aws-sdk-go-v2 v1.27.0 github.com/aws/aws-sdk-go-v2/credentials v1.17.15 github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.21.7 + github.com/coreos/go-oidc/v3 v3.11.0 github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/render v1.0.3 github.com/go-ldap/ldap/v3 v3.4.8 + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/common v0.53.0 github.com/stretchr/testify v1.9.0 github.com/wangli1030/saml v0.4.7 go.etcd.io/etcd/client/v3 v3.5.13 go.uber.org/zap v1.27.0 + golang.org/x/oauth2 v0.21.0 k8s.io/api v0.30.0 k8s.io/apimachinery v0.30.0 k8s.io/client-go v0.30.0 @@ -50,6 +53,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -73,14 +77,12 @@ require ( go.etcd.io/etcd/api/v3 v3.5.13 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/grpc v1.59.0 // indirect diff --git a/go.sum b/go.sum index cd16280..dc387f4 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= @@ -48,6 +50,8 @@ github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +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-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -65,7 +69,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -196,14 +201,14 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -215,10 +220,10 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -234,23 +239,25 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -259,14 +266,12 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= diff --git a/internal/rbac/user.go b/internal/rbac/user.go index bba3773..42d6fae 100644 --- a/internal/rbac/user.go +++ b/internal/rbac/user.go @@ -5,17 +5,45 @@ */ package rbac -import "net/http" +import ( + "net/http" + + "github.com/golang-jwt/jwt/v4" +) type User struct { - Surname string `json:"Surname,omitempty"` - Givenname string `json:"Givenname,omitempty"` - UID string `json:"UID,omitempty"` - Displayname string `json:"Displayname,omitempty"` - Emailaddress string `json:"Emailaddress,omitempty"` + Surname string `json:"Surname,omitempty"` + Givenname string `json:"Givenname,omitempty"` + UID string `json:"UID,omitempty"` + Displayname string `json:"Displayname,omitempty"` + Emailaddress string `json:"Emailaddress,omitempty"` + AdGroups []string `json:"roles,omitempty"` +} + +type ThelivUser struct { + jwt.RegisteredClaims + DisplayName string `json:"displayName,omitempty"` + GivenName string `json:"givenName,omitempty"` + JobTitle string `json:"jobTitle,omitempty"` + Mail string `json:"mail,omitempty"` + Email string `json:"email,omitempty"` + Surname string `json:"surname,omitempty"` + UserPrincipalName string `json:"userPrincipalName,omitempty"` + Upn string `json:"upn"` + Groups []string `json:"groups,omitempty"` +} + +type UserInfo struct { + Name string `json:"name"` + FamilyName string `json:"family_name"` + GivenName string `json:"given_name"` + Email string `json:"email"` + Upn string `json:"upn"` + OnBehalf string `json:"onbehalf"` // on behalf of user name -- corpid + Roles []string `json:"roles"` + jwt.StandardClaims } type RBACInfo interface { - GetUser(r *http.Request) (*User, error) - GetADgroups(r *http.Request, id string) ([]string, error) + GetUser(r *http.Request, getAd bool) (*User, error) } diff --git a/main.go b/main.go index 2a0314e..e25d3cd 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "net/http" "strings" - "github.com/fidelity/theliv/pkg/auth/samlmethod" + "github.com/fidelity/theliv/pkg/auth/oidcmethod" "github.com/fidelity/theliv/pkg/config" log "github.com/fidelity/theliv/pkg/log" "github.com/fidelity/theliv/pkg/router" @@ -42,7 +42,7 @@ func main() { } conf.LoadConfigs() - samlmethod.Init() + oidcmethod.InitAuth() r := router.NewRouter() diff --git a/pkg/auth/authmiddleware/authmiddleware.go b/pkg/auth/authmiddleware/authmiddleware.go index 77a689c..54862c4 100644 --- a/pkg/auth/authmiddleware/authmiddleware.go +++ b/pkg/auth/authmiddleware/authmiddleware.go @@ -12,9 +12,8 @@ import ( "github.com/fidelity/theliv/internal/rbac" "github.com/fidelity/theliv/pkg/auth/localmethod" - "github.com/fidelity/theliv/pkg/auth/samlmethod" + "github.com/fidelity/theliv/pkg/auth/oidcmethod" "github.com/fidelity/theliv/pkg/config" - "github.com/wangli1030/saml/samlsp" ) var ErrNotThisAuth = errors.New("not this Auth method") @@ -23,8 +22,8 @@ var authMethod rbac.RBACInfo func StartAuth(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //whitelist path - auth := config.GetThelivConfig().Auth - if r.URL.Path == "/theliv-api/v1/health" || r.URL.Path == "/theliv-api/v1/metrics" || r.URL.Path == getUrlPath(auth.AcrURL) || r.URL.Path == getUrlPath(auth.MetadataURL) { + oidc := config.GetThelivConfig().Oidc + if r.URL.Path == "/theliv-api/v1/health" || r.URL.Path == "/theliv-api/v1/metrics" || r.URL.Path == getUrlPath(oidc.CallBack) { handler.ServeHTTP(w, r) return } @@ -45,10 +44,10 @@ func StartAuth(handler http.Handler) http.Handler { return } if err.Error() == ErrNotThisAuth.Error() { - //saml auth - r, err = samlmethod.CheckAuthorization(r) + //oidc auth + r, err = oidcmethod.CheckAuthorization(r) if err == nil { - authMethod = samlmethod.Samlinfo{} + authMethod = oidcmethod.OIDC{} ok, err := checkRBAC(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -61,8 +60,8 @@ func StartAuth(handler http.Handler) http.Handler { } return } - if err == samlsp.ErrNoSession { - samlmethod.HandleStartAuthFlow(w, r) + if err == oidcmethod.ErrNoIDFound { + oidcmethod.HandleStartAuthFlow(w, r) return } } @@ -70,12 +69,8 @@ func StartAuth(handler http.Handler) http.Handler { }) } -func GetUser(r *http.Request) (*rbac.User, error) { - return authMethod.GetUser(r) -} - -func GetADgroups(r *http.Request, id string) ([]string, error) { - return authMethod.GetADgroups(r, id) +func GetUser(r *http.Request, getAds bool) (*rbac.User, error) { + return authMethod.GetUser(r, getAds) } func getUrlPath(p string) string { diff --git a/pkg/auth/authmiddleware/checkauth.go b/pkg/auth/authmiddleware/checkauth.go index 7af4d23..d899c00 100644 --- a/pkg/auth/authmiddleware/checkauth.go +++ b/pkg/auth/authmiddleware/checkauth.go @@ -6,10 +6,10 @@ package authmiddleware import ( + "context" "errors" "net/http" "strings" - "context" "github.com/fidelity/theliv/pkg/config" "github.com/fidelity/theliv/pkg/database/etcd" @@ -42,7 +42,7 @@ func getPath(ctx context.Context, role string) ([]string, error) { } func checkRBAC(r *http.Request) (bool, error) { - user, err := GetUser(r) + user, err := GetUser(r, true) if err != nil { return false, err } @@ -64,10 +64,8 @@ func checkRBAC(r *http.Request) (bool, error) { if err != nil { return false, err } - adgroups, err := GetADgroups(r, user.UID) - if err != nil { - return false, err - } + adgroups := user.AdGroups + roles = append(roles, adgroups...) var grantPath []string for _, role := range roles { diff --git a/pkg/auth/localmethod/localmethod.go b/pkg/auth/localmethod/localmethod.go index 3c889da..a06a387 100644 --- a/pkg/auth/localmethod/localmethod.go +++ b/pkg/auth/localmethod/localmethod.go @@ -33,7 +33,7 @@ func CheckAuthorization(r *http.Request) (*http.Request, error) { type Localinfo struct { } -func (Localinfo) GetUser(r *http.Request) (*rbac.User, error) { +func (Localinfo) GetUser(r *http.Request, getAd bool) (*rbac.User, error) { userinfo := &rbac.User{} accesskey := r.Header.Get("ACCESSKEY") err := etcd.GetObject(accesskeyPrefix+accesskey, userinfo) @@ -42,10 +42,3 @@ func (Localinfo) GetUser(r *http.Request) (*rbac.User, error) { } return nil, err } - -func (Localinfo) GetADgroups(r *http.Request, id string) ([]string, error) { - if true { - return nil, nil - } - return nil, errors.New("not Authorized") -} diff --git a/pkg/auth/oidcmethod/callback.go b/pkg/auth/oidcmethod/callback.go new file mode 100644 index 0000000..328655a --- /dev/null +++ b/pkg/auth/oidcmethod/callback.go @@ -0,0 +1,157 @@ +/* + * Copyright FMR LLC + * + * SPDX-License-Identifier: Apache + */ +package oidcmethod + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/fidelity/theliv/internal/rbac" + "github.com/go-chi/chi/v5" + "github.com/golang-jwt/jwt/v4" + "golang.org/x/oauth2" + + "github.com/fidelity/theliv/pkg/config" + log "github.com/fidelity/theliv/pkg/log" + + theliverr "github.com/fidelity/theliv/pkg/err" +) + +var verifier *oidc.IDTokenVerifier + +func SSO(r chi.Router) { + r.Get("/callback", callback) +} + +func callback(w http.ResponseWriter, r *http.Request) { + if err := ExchangeToken(w, r); err != nil { + processError(w, r, err) + return + } + oicdConfig := config.GetThelivConfig().Oidc + host := oicdConfig.CallBackHost + // redirect, by default to / + state := r.URL.Query().Get("state") + + http.Redirect(w, r, host+state, http.StatusFound) +} + +func ExchangeToken(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + oauth2Token, err := getOauthConfig(r).Exchange(r.Context(), r.URL.Query().Get("code")) + if err != nil { + msg := "failed to exchange token" + log.SWithContext(ctx).Error(msg) + return theliverr.NewCommonError(ctx, 1, msg) + } + rawIDToken, ok := oauth2Token.Extra(IDTokenKey).(string) + if !ok { + msg := "No id_token field in oauth2 token." + log.SWithContext(ctx).Error(msg) + return theliverr.NewCommonError(ctx, 1, msg) + } + idtoken, err := verify(ctx, rawIDToken) + if err != nil { + msg := "failed to verify ID token" + log.SWithContext(ctx).Error(msg) + return theliverr.NewCommonError(ctx, 1, msg) + } + thelivUser := &rbac.ThelivUser{} + if err := idtoken.Claims(thelivUser); err != nil { + msg := "failed to unmarshal user from id_token" + log.SWithContext(ctx).Error(msg) + return theliverr.NewCommonError(ctx, 1, msg) + } + // user id + user := rbac.UserInfo{} + user.Email = thelivUser.Email + if thelivUser.ExpiresAt != nil { + user.ExpiresAt = thelivUser.ExpiresAt.UnixMilli() + } + user.FamilyName = thelivUser.Surname + user.GivenName = thelivUser.GivenName + user.Issuer = thelivUser.Issuer + user.Name = thelivUser.DisplayName + user.Upn = thelivUser.Upn + user.Id = strings.ToLower(strings.Split(user.Upn, "@")[0]) + user.Roles = thelivUser.Groups + + // set access_token cookie + acc := &http.Cookie{ + Path: "/", + Name: AccessTokenKey, + Value: oauth2Token.AccessToken, + Expires: oauth2Token.Expiry, + Secure: r.TLS != nil, + HttpOnly: true, + } + http.SetCookie(w, acc) + + return setUserCookie(r.Context(), user, w) +} + +func getOauthConfig(r *http.Request) *oauth2.Config { + ctx := r.Context() + host := r.Host + if oc, ok := oauthConfigs[host]; ok { + return oc + } + oicdConfig := config.GetThelivConfig().Oidc + log.SWithContext(ctx).Infof("oauth-config for host %s does not exist, create new one", host) + // create new one + callback := GetHost(r) + oicdConfig.CallBack + oauthConfigs[host] = &oauth2.Config{ + ClientID: oicdConfig.ClientID, + ClientSecret: oicdConfig.ClientSecret, + Endpoint: provider.Endpoint(), + RedirectURL: callback, + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } + log.SWithContext(ctx).Infof("successfully created new oauth config") + return oauthConfigs[host] +} + +func setUserCookie(ctx context.Context, user rbac.UserInfo, w http.ResponseWriter) error { + + userinfo, err := userJwt(ctx, &user) + if err != nil { + return err + } + //set id tokens + cookies := splitCookie(IDTokenKey, userinfo) + for _, ck := range cookies { + http.SetCookie(w, ck) + } + return nil +} + +func userJwt(ctx context.Context, user *rbac.UserInfo) (string, error) { + + // sign userinfo JWT + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, user) + jwt, err := jwtToken.SignedString(jwtKey) + if err != nil { + msg := "Failed to sign jwt" + log.SWithContext(ctx).Error() + return "", theliverr.NewCommonError(ctx, 1, msg) + } + return jwt, nil +} + +func GetHost(req *http.Request) string { + proto := "https" + if req.TLS == nil { + proto = "http" + } + return fmt.Sprintf("%s://%s", proto, req.Host) +} + +var verify = func(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) { + return verifier.Verify(ctx, rawIDToken) +} diff --git a/pkg/auth/oidcmethod/cookie.go b/pkg/auth/oidcmethod/cookie.go new file mode 100644 index 0000000..033ef13 --- /dev/null +++ b/pkg/auth/oidcmethod/cookie.go @@ -0,0 +1,100 @@ +/* + * Copyright FMR LLC + * + * SPDX-License-Identifier: Apache + */ +package oidcmethod + +import ( + "context" + "fmt" + "math" + "net/http" + "strconv" + "strings" + "time" + + theliverr "github.com/fidelity/theliv/pkg/err" + log "github.com/fidelity/theliv/pkg/log" + "github.com/go-chi/render" +) + +const ( + maxValueLength = 3900 +) + +func splitCookie(key string, value string) []*http.Cookie { + cookies := []*http.Cookie{} + valueLength := len(value) + numberOfChunks := int(math.Ceil(float64(valueLength) / float64(maxValueLength))) + var end int + for i, j := 0, 0; i < valueLength; i, j = i+maxValueLength, j+1 { + end = i + maxValueLength + if end > valueLength { + end = valueLength + } + cookie := &http.Cookie{ + Path: "/", + Expires: time.Now().Add(time.Hour), + Secure: true, + HttpOnly: true, + } + if j == 0 && numberOfChunks == 1 { + cookie.Name = key + cookie.Value = value[i:end] + } else if j == 0 { + cookie.Name = key + cookie.Value = fmt.Sprintf("%d:%s", numberOfChunks, value[i:end]) + } else { + cookie.Name = fmt.Sprintf("%s-%d", key, j) + cookie.Value = string(value[i:end]) + } + cookies = append(cookies, cookie) + } + return cookies +} + +func joinCookies(ctx context.Context, key string, cookieList []*http.Cookie) (string, error) { + cookies := make(map[string]string) + for _, cookie := range cookieList { + if !strings.HasPrefix(cookie.Name, key) { + continue + } + cookies[cookie.Name] = cookie.Value + } + + var sb strings.Builder + var numOfChunks int + var err error + var token string + var ok bool + if token, ok = cookies[key]; !ok { + msg := "failed to retrieve id_token from cookies" + log.SWithContext(ctx).Warnf(msg) + return "", theliverr.NewCommonError(ctx, 1, msg) + } + parts := strings.Split(token, ":") + + if len(parts) == 2 { + if numOfChunks, err = strconv.Atoi(parts[0]); err != nil { + return "", err + } + sb.WriteString(parts[1]) + } else if len(parts) == 1 { + numOfChunks = 1 + sb.WriteString(parts[0]) + } else { + log.SWithContext(ctx).Warn("invalid cookie %s") + return "", fmt.Errorf("invalid cookie for key %s", key) + } + + for i := 1; i < numOfChunks; i++ { + sb.WriteString(cookies[fmt.Sprintf("%s-%d", key, i)]) + } + return sb.String(), nil +} + +func processError(w http.ResponseWriter, r *http.Request, err error) { + w.WriteHeader(theliverr.GetStatusCode(err)) + render.JSON(w, r, err) +} diff --git a/pkg/auth/oidcmethod/oidcmethod.go b/pkg/auth/oidcmethod/oidcmethod.go new file mode 100644 index 0000000..06c6644 --- /dev/null +++ b/pkg/auth/oidcmethod/oidcmethod.go @@ -0,0 +1,136 @@ +/* + * Copyright FMR LLC + * + * SPDX-License-Identifier: Apache + */ +package oidcmethod + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/fidelity/theliv/internal/rbac" + "github.com/fidelity/theliv/pkg/config" + log "github.com/fidelity/theliv/pkg/log" + "github.com/go-chi/render" + "github.com/golang-jwt/jwt/v4" + "golang.org/x/oauth2" +) + +const ( + AccessTokenKey string = "access_token" + IDTokenKey string = "id_token" +) + +var provider *oidc.Provider + +var oauthConfigs = make(map[string]*oauth2.Config) +var jwtKey []byte +var ErrNoIDFound = errors.New("no id token found") + +type OIDC struct { +} + +// Initialize oidc config +func InitAuth() error { + if provider != nil { + return nil + } + oicdConfig := config.GetThelivConfig().Oidc + p, err := oidc.NewProvider(context.Background(), oicdConfig.OidcProvider) + if err != nil { + log.S().Errorf("Unable to initialize oidc config: %v", err) + return err + } + provider = p + + oidcConfig := &oidc.Config{ + ClientID: oicdConfig.ClientID, + } + verifier = provider.Verifier(oidcConfig) + jwtKey = []byte(oicdConfig.ClientSecret) + return nil +} + +func (OIDC) GetUser(r *http.Request, getAd bool) (*rbac.User, error) { + c, err := joinCookies(r.Context(), IDTokenKey, r.Cookies()) + if err != nil { + log.SWithContext(r.Context()).Warnf("Cookie %v does not exist: %v", IDTokenKey, err) + return nil, err + } + extract, err := userFromToken(r.Context(), c) + if err != nil { + return nil, err + } + user := rbac.User{} + user.UID = extract.Id + user.Surname = extract.FamilyName + user.Givenname = extract.GivenName + user.Displayname = extract.Name + user.Emailaddress = extract.Email + if getAd { + user.AdGroups = extract.Roles + } + return &user, nil +} + +func userFromToken(ctx context.Context, auth string) (*rbac.UserInfo, error) { + user := &rbac.UserInfo{} + token, err := jwt.ParseWithClaims(auth, user, func(t *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + if err != nil { + log.SWithContext(ctx).Errorf("authorization code/cookie invalid") + return nil, err + } + c, ok := token.Claims.(*rbac.UserInfo) + if !ok { + msg := "cannot unmarshal ID token claim" + err := fmt.Errorf(msg) + log.SWithContext(ctx).Errorf(msg) + return nil, err + } + if e := c.Valid(); e != nil { + log.SWithContext(ctx).Errorf("failed to validate token") + return nil, e + } + return user, nil +} + +func CheckAuthorization(r *http.Request) (*http.Request, error) { + for _, cookie := range r.Cookies() { + if strings.HasPrefix(cookie.Name, IDTokenKey) { + return r, nil + } + } + return r, ErrNoIDFound +} + +func HandleStartAuthFlow(w http.ResponseWriter, r *http.Request) { + state := getRedirectUrl(r.Header.Get("redirect")) + if state == "" { + state = "/" + } + // redirect to authorization url + url := getOauthConfig(r).AuthCodeURL(state) + w.Header().Add("X-Location", url) + w.WriteHeader(http.StatusUnauthorized) + render.JSON(w, r, struct { + Error string `json:"error"` + Code int `json:"code"` + Login bool `json:"login"` + }{url, http.StatusUnauthorized, true}) +} + +func getRedirectUrl(url string) (redirect string) { + redirect = "/theliv/" + path := strings.Split(url, redirect) + if len(path) == 2 { + redirect = redirect + path[1] + } + return +} diff --git a/pkg/auth/samlmethod/samlmethod.go b/pkg/auth/samlmethod/samlmethod.go index a90546d..debe9b7 100644 --- a/pkg/auth/samlmethod/samlmethod.go +++ b/pkg/auth/samlmethod/samlmethod.go @@ -178,7 +178,7 @@ func getRedirectUrl(url string) (redirect string) { type Samlinfo struct { } -func (Samlinfo) GetUser(r *http.Request) (*rbac.User, error) { +func (Samlinfo) GetUser(r *http.Request, getAds bool) (*rbac.User, error) { if session := samlsp.SessionFromContext(r.Context()); session != nil { // this will panic if we have the wrong type of Session, and that is OK. emptyResult := []string{""} diff --git a/pkg/config/config.go b/pkg/config/config.go index 0dcbb5c..3e6a87e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -59,6 +59,7 @@ type ThelivConfig struct { ClusterDir string `json:"clusterDir,omitempty"` Datadog *DatadogConfig `json:"datadog,omitempty"` Auth *AuthConfig `json:"auth,omitempty"` + Oidc *OidcConfig `json:"oidc,omitempty"` Prometheus *PrometheusConfig `json:"prometheus,omitempty"` ProblemLevel *ProblemLevelConfig `json:"problemlevel,omitempty"` Ldap *LdapConfig @@ -110,6 +111,14 @@ type AuthConfig struct { ClientSecret string `json:"clientSecret"` } +type OidcConfig struct { + OidcProvider string `json:"oidcProvider"` + CallBack string `json:"callBack"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + CallBackHost string `json:"callBackHost"` +} + func (c *AuthConfig) ToMaskString() string { return fmt.Sprintf(`Auth config: CertPath: %v, diff --git a/pkg/config/etcd.go b/pkg/config/etcd.go index 6f7ea2d..ea9a756 100644 --- a/pkg/config/etcd.go +++ b/pkg/config/etcd.go @@ -39,6 +39,10 @@ func (ecl *EtcdConfigLoader) LoadConfigs() { log.S().Errorf("Failed to load auth config, error is %v\n", err) } + if err := ecl.loadOidcConfig(); err != nil { + log.S().Errorf("Failed to load oidc config, error is %v\n", err) + } + if err := ecl.loadPrometheusConfig(); err != nil { log.S().Errorf("Failed to load prometheus config, error is %v\n", err) } @@ -138,6 +142,17 @@ func (ecl *EtcdConfigLoader) loadDatadogConfig() error { return nil } +func (ecl *EtcdConfigLoader) loadOidcConfig() error { + conf := &OidcConfig{} + err := driver.GetObjectWithSub(context.Background(), driver.OIDC_KEY, conf) + if err != nil { + return err + } + thelivConfig.Oidc = conf + log.S().Infof("Successfully load oidc config, provider is: %s.\n", conf.OidcProvider) + return nil +} + func (ecl *EtcdConfigLoader) loadAuthConfig() error { conf := &AuthConfig{} err := driver.GetObjectWithSub(context.Background(), driver.THELIV_AUTH_KEY, conf) diff --git a/pkg/database/etcd/client.go b/pkg/database/etcd/client.go index d146296..749c418 100644 --- a/pkg/database/etcd/client.go +++ b/pkg/database/etcd/client.go @@ -30,6 +30,7 @@ const ( THELIV_CONFIG_KEY string = "/theliv/config" DATADOG_CONFIG_KEY string = "/theliv/config/datadog" THELIV_AUTH_KEY string = "/theliv/config/authconf" + OIDC_KEY string = "/theliv/config/oidc" CLUSTERS_KEY string = "/theliv/clusters" PROMETHEUS_GLOBAL_CONFIG_KEY string = "/theliv/config/prometheus" THELIV_LEVEL_CONFIG_KEY string = "/theliv/config/levelconf" diff --git a/pkg/router/feedback.go b/pkg/router/feedback.go index af9082e..9114f3a 100644 --- a/pkg/router/feedback.go +++ b/pkg/router/feedback.go @@ -40,7 +40,7 @@ func SubmitFeedback(r chi.Router) { key := "/theliv/feedbacks/" + timestr - user, err := authmiddleware.GetUser(r) + user, err := authmiddleware.GetUser(r, false) if err != nil { processError(w, r, err) } @@ -49,7 +49,7 @@ func SubmitFeedback(r chi.Router) { Time: currentTime, Message: d.Message, } - + err = etcd.Put(key, data) if err != nil { processError(w, r, err) diff --git a/pkg/router/router.go b/pkg/router/router.go index e9ae153..0f0db20 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -7,7 +7,7 @@ package router import ( "github.com/fidelity/theliv/pkg/auth/authmiddleware" - "github.com/fidelity/theliv/pkg/auth/samlmethod" + "github.com/fidelity/theliv/pkg/auth/oidcmethod" "github.com/fidelity/theliv/pkg/err" log "github.com/fidelity/theliv/pkg/log" "github.com/fidelity/theliv/pkg/metrics" @@ -41,7 +41,7 @@ func NewRouter() *chi.Mux { r.Route("/theliv-api/v1", Route) // saml route - r.Handle("/auth/saml/*", samlmethod.GetSP()) + r.Route("/auth/", oidcmethod.SSO) return r diff --git a/pkg/router/userinfo.go b/pkg/router/userinfo.go index 1113837..f2c70f0 100644 --- a/pkg/router/userinfo.go +++ b/pkg/router/userinfo.go @@ -16,7 +16,7 @@ import ( func Userinfo(r chi.Router) { r.Get("/", func(w http.ResponseWriter, req *http.Request) { - user, err := authmiddleware.GetUser(req) + user, err := authmiddleware.GetUser(req, false) if err != nil { http.Error(w, com.NoUserInfo, http.StatusInternalServerError) } else if empty := processEmpty(w, req, user); !empty {