diff --git a/deploy/ansible/roles/sun/templates/sun-api-conf.j2 b/deploy/ansible/roles/sun/templates/sun-api-conf.j2 index 4c486517d..9ee6ee667 100644 --- a/deploy/ansible/roles/sun/templates/sun-api-conf.j2 +++ b/deploy/ansible/roles/sun/templates/sun-api-conf.j2 @@ -103,5 +103,14 @@ icaro.sun_api.email_smtp_password is defined %} "verb": "POST", "endpoint": "/api/users" }] + }, + "oidc": { + "issuer": "{{ icaro.sun_api.oidc_issuer | default('') }}", + "client_id": "{{ icaro.sun_api.oidc_client_id | default('') }}", + "client_secret": "{{ icaro.sun_api.oidc_client_secret | default('') }}", + "redirect_uri": "{{ icaro.sun_api.oidc_redirect_uri | default('http://localhost:8080/api/auth/oidc/callback') }}", + "frontend_url": "{{ icaro.sun_api.oidc_frontend_url | default('http://localhost:8081') }}", + "scopes": {{ icaro.sun_api.oidc_scopes | default(['openid', 'profile', 'email', 'roles', 'urn:logto:scope:organizations', 'urn:logto:scope:organization_roles']) | to_json }}, + "role_mapping": {{ icaro.sun_api.oidc_role_mapping | default(['super admin:admin', 'admin:reseller']) | to_json }} } } diff --git a/go.mod b/go.mod index 8488c04ea..da47cf05f 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,15 @@ go 1.15 require ( github.com/appleboy/gofight/v2 v2.1.2 github.com/avct/uasurfer v0.0.0-20180817072212-dc0ec4fd1e87 + github.com/coreos/go-oidc/v3 v3.5.0 github.com/fatih/structs v1.1.0 github.com/gin-contrib/cors v1.3.0 github.com/gin-gonic/gin v1.7.7 github.com/jinzhu/gorm v1.9.8 github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5 - github.com/stretchr/testify v1.4.0 + github.com/stretchr/testify v1.7.0 + golang.org/x/oauth2 v0.3.0 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) diff --git a/go.sum b/go.sum index 679fd1c69..14d40578d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= @@ -14,6 +15,8 @@ github.com/avct/uasurfer v0.0.0-20180817072212-dc0ec4fd1e87 h1:TUuuMI5Sxsw5IVVu3 github.com/avct/uasurfer v0.0.0-20180817072212-dc0ec4fd1e87/go.mod h1:noBAuukeYOXa0aXGqxr24tADqkwDO2KRD15FsuaZ5a8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= +github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= 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= @@ -35,6 +38,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= @@ -55,12 +60,17 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -130,23 +140,29 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/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= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -157,12 +173,23 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= +golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -172,26 +199,46 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= @@ -208,6 +255,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sun/sun-api/configuration/configuration.go b/sun/sun-api/configuration/configuration.go index 633d464e3..a2600d111 100644 --- a/sun/sun-api/configuration/configuration.go +++ b/sun/sun-api/configuration/configuration.go @@ -53,6 +53,15 @@ type Configuration struct { Origins []string `json:"origins"` Methods []string `json:"methods"` } `json:"cors"` + OIDC struct { + Issuer string `json:"issuer"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURI string `json:"redirect_uri"` + FrontendURL string `json:"frontend_url"` + Scopes []string `json:"scopes"` + RoleMapping []string `json:"role_mapping"` + } `json:"oidc"` Disclaimers struct { TermsOfUse string `json:"terms_of_use"` MarketingUse string `json:"marketing_use"` @@ -223,6 +232,28 @@ func Init(ConfigFilePtr *string) { Config.Survey.Url = os.Getenv("SURVEY_URL") } + if os.Getenv("OIDC_ISSUER") != "" { + Config.OIDC.Issuer = os.Getenv("OIDC_ISSUER") + } + if os.Getenv("OIDC_CLIENT_ID") != "" { + Config.OIDC.ClientID = os.Getenv("OIDC_CLIENT_ID") + } + if os.Getenv("OIDC_CLIENT_SECRET") != "" { + Config.OIDC.ClientSecret = os.Getenv("OIDC_CLIENT_SECRET") + } + if os.Getenv("OIDC_REDIRECT_URI") != "" { + Config.OIDC.RedirectURI = os.Getenv("OIDC_REDIRECT_URI") + } + if os.Getenv("OIDC_FRONTEND_URL") != "" { + Config.OIDC.FrontendURL = os.Getenv("OIDC_FRONTEND_URL") + } + if os.Getenv("OIDC_SCOPES") != "" { + Config.OIDC.Scopes = strings.Split(os.Getenv("OIDC_SCOPES"), " ") + } + if os.Getenv("OIDC_ROLE_MAPPING") != "" { + Config.OIDC.RoleMapping = strings.Split(os.Getenv("OIDC_ROLE_MAPPING"), " ") + } + Config.CaptivePortal.LogoContents = "" if _, err := os.Stat(Config.CaptivePortal.Logo); err == nil { if data, errRead := ioutil.ReadFile(Config.CaptivePortal.Logo); errRead == nil { diff --git a/sun/sun-api/main.go b/sun/sun-api/main.go index 9033491e8..d21c83442 100644 --- a/sun/sun-api/main.go +++ b/sun/sun-api/main.go @@ -50,6 +50,10 @@ func DefineAPI(router *gin.Engine) { api.POST("/login", methods.Login) api.POST("/logout", methods.Logout) + api.GET("/auth/oidc/config", methods.GetOIDCConfig) + api.GET("/auth/oidc/login", methods.OIDCLogin) + api.GET("/auth/oidc/callback", methods.OIDCCallback) + api.POST("/auth/oidc/exchange", methods.OIDCExchange) api.Use(middleware.AAWall) { diff --git a/sun/sun-api/methods/authentication.go b/sun/sun-api/methods/authentication.go index 3606ab3f8..b228feab5 100644 --- a/sun/sun-api/methods/authentication.go +++ b/sun/sun-api/methods/authentication.go @@ -23,14 +23,21 @@ package methods import ( + "context" "crypto/md5" + "crypto/rand" "crypto/sha256" + "encoding/base64" "fmt" "net/http" + "strings" + "sync" "time" + "github.com/coreos/go-oidc/v3/oidc" "github.com/gin-gonic/gin" _ "github.com/jinzhu/gorm/dialects/mysql" + "golang.org/x/oauth2" "github.com/nethesis/icaro/sun/sun-api/configuration" "github.com/nethesis/icaro/sun/sun-api/database" @@ -38,6 +45,12 @@ import ( "github.com/nethesis/icaro/sun/sun-api/utils" ) +// Global cleanup context and cancel function +var ( + cleanupCtx context.Context + cleanupCancel context.CancelFunc +) + func Login(c *gin.Context) { var account models.Account var subscription models.Subscription @@ -121,3 +134,541 @@ func Logout(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "success"}) } } + +// OIDCStateStore manages OIDC state tokens with expiration +type OIDCStateStore struct { + mu sync.RWMutex + states map[string]time.Time +} + +// OIDCCodeStore manages temporary codes for secure token exchange +type OIDCCodeStore struct { + mu sync.RWMutex + codes map[string]OIDCCodeData +} + +type OIDCCodeData struct { + Token string + AccountID int + Expires time.Time +} + +// Global state store instance +var stateStore = &OIDCStateStore{ + states: make(map[string]time.Time), +} + +// Global code store instance +var codeStore = &OIDCCodeStore{ + codes: make(map[string]OIDCCodeData), +} + +// StoreState stores a state token with expiration time +func (s *OIDCStateStore) StoreState(state string, expiration time.Time) { + s.mu.Lock() + defer s.mu.Unlock() + s.states[state] = expiration +} + +// ValidateAndRemoveState checks if state is valid and removes it +func (s *OIDCStateStore) ValidateAndRemoveState(state string) bool { + s.mu.Lock() + defer s.mu.Unlock() + + expiration, exists := s.states[state] + if !exists { + return false + } + + // Remove the state regardless of expiration (one-time use) + delete(s.states, state) + + // Check if state has expired + if time.Now().After(expiration) { + return false + } + + return true +} + +// CleanupExpiredStates removes expired states (should be called periodically) +func (s *OIDCStateStore) CleanupExpiredStates() { + s.mu.Lock() + defer s.mu.Unlock() + + now := time.Now() + for state, expiration := range s.states { + if now.After(expiration) { + delete(s.states, state) + } + } +} + +// StoreCode stores a temporary code with token and account data +func (c *OIDCCodeStore) StoreCode(code string, token string, accountID int, expiration time.Time) { + c.mu.Lock() + defer c.mu.Unlock() + c.codes[code] = OIDCCodeData{ + Token: token, + AccountID: accountID, + Expires: expiration, + } +} + +// ValidateAndRemoveCode checks if code is valid and removes it (one-time use) +func (c *OIDCCodeStore) ValidateAndRemoveCode(code string) (OIDCCodeData, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + data, exists := c.codes[code] + if !exists { + return OIDCCodeData{}, false + } + + // Remove the code regardless of expiration (one-time use) + delete(c.codes, code) + + // Check if code has expired + if time.Now().After(data.Expires) { + return OIDCCodeData{}, false + } + + return data, true +} + +// CleanupExpiredCodes removes expired codes +func (c *OIDCCodeStore) CleanupExpiredCodes() { + c.mu.Lock() + defer c.mu.Unlock() + + now := time.Now() + for code, data := range c.codes { + if now.After(data.Expires) { + delete(c.codes, code) + } + } +} + +// Initialize cleanup routine with graceful shutdown support +func init() { + cleanupCtx, cleanupCancel = context.WithCancel(context.Background()) + + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-cleanupCtx.Done(): + return + case <-ticker.C: + stateStore.CleanupExpiredStates() + codeStore.CleanupExpiredCodes() + } + } + }() +} + +// StopCleanupRoutine stops the background cleanup goroutine gracefully +func StopCleanupRoutine() { + if cleanupCancel != nil { + cleanupCancel() + } +} + +// generateSecureState generates a cryptographically secure random state string +func generateSecureState() (string, error) { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// createOAuth2Config creates the OAuth2 configuration based on the OIDC config +func createOAuth2Config(config configuration.Configuration, provider *oidc.Provider) oauth2.Config { + scopes := []string{oidc.ScopeOpenID, "profile", "email"} + + // Add configured scopes if available, otherwise use defaults + if len(config.OIDC.Scopes) > 0 { + scopes = config.OIDC.Scopes + } else { + // Default scopes for Logto + scopes = append(scopes, "roles", "urn:logto:scope:organizations", "urn:logto:scope:organization_roles") + } + + return oauth2.Config{ + ClientID: config.OIDC.ClientID, + ClientSecret: config.OIDC.ClientSecret, + RedirectURL: config.OIDC.RedirectURI, + Endpoint: provider.Endpoint(), + Scopes: scopes, + } +} + +// extractRolesFromClaims extracts roles from various possible claim fields +func extractRolesFromClaims(claims map[string]interface{}) []string { + var roles []string + + // List of possible role claim fields + roleFields := []string{"roles", "role", "groups", "organizations", "organization_roles"} + + for _, field := range roleFields { + if fieldValue, exists := claims[field]; exists { + // Handle array of roles + if roleSlice, ok := fieldValue.([]interface{}); ok { + for _, role := range roleSlice { + if roleStr, ok := role.(string); ok { + roles = append(roles, roleStr) + } + } + } + // Handle single role as string + if roleStr, ok := fieldValue.(string); ok { + roles = append(roles, roleStr) + } + } + + // If we found roles, stop looking + if len(roles) > 0 { + break + } + } + + return roles +} + +// mapRoleToIcaro maps external roles to Icaro roles based on configuration +// Returns empty string if role is not authorized for OIDC login +func mapRoleToIcaro(externalRoles []string, config configuration.Configuration) string { + // Parse role mapping from configuration: "external:internal" format + roleMapping := make(map[string]string) + + for _, mapping := range config.OIDC.RoleMapping { + parts := strings.Split(mapping, ":") + if len(parts) == 2 { + roleMapping[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + + // If no role mapping configured, deny access - no default mappings for security + if len(roleMapping) == 0 { + return "" + } + + // Check for role mappings based on configured roles only + for _, externalRole := range externalRoles { + for configRole, icaroRole := range roleMapping { + if strings.EqualFold(externalRole, configRole) { + return icaroRole + } + } + } + + // Return empty string if no authorized role found + return "" +} + +// validateOIDCConfig validates that OIDC configuration is complete +func validateOIDCConfig(config configuration.Configuration) error { + if config.OIDC.Issuer == "" { + return fmt.Errorf("OIDC issuer not configured") + } + if config.OIDC.ClientID == "" { + return fmt.Errorf("OIDC client ID not configured") + } + if config.OIDC.ClientSecret == "" { + return fmt.Errorf("OIDC client secret not configured") + } + if config.OIDC.RedirectURI == "" { + return fmt.Errorf("OIDC redirect URI not configured") + } + if config.OIDC.FrontendURL == "" { + return fmt.Errorf("OIDC frontend URL not configured") + } + return nil +} + +func GetOIDCConfig(c *gin.Context) { + config := configuration.Config + + // Return only public configuration information + response := gin.H{ + "enabled": config.OIDC.Issuer != "" && config.OIDC.ClientID != "", + } + + // Only add provider name if OIDC is properly configured + if config.OIDC.Issuer != "" && config.OIDC.ClientID != "" { + response["provider_name"] = "My Nethesis" + } + + c.JSON(http.StatusOK, response) +} + +func OIDCLogin(c *gin.Context) { + config := configuration.Config + + // Validate OIDC configuration + if err := validateOIDCConfig(config); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "OIDC configuration error", "error": err.Error()}) + return + } + + ctx := context.Background() + + // Always use auto-discovery for endpoints + provider, err := oidc.NewProvider(ctx, config.OIDC.Issuer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to get OIDC provider", "error": err.Error()}) + return + } + + oauth2Config := createOAuth2Config(config, provider) + + // Generate secure random state + state, err := generateSecureState() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to generate state", "error": err.Error()}) + return + } + + // Store state with expiration (valid for 10 minutes) + stateStore.StoreState(state, time.Now().Add(10*time.Minute)) + + // Get authorization URL + authURL := oauth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline) + + // Redirect to authorization URL + c.Redirect(http.StatusTemporaryRedirect, authURL) +} + +func OIDCCallback(c *gin.Context) { + config := configuration.Config + ctx := context.Background() + + // Validate OIDC configuration + if err := validateOIDCConfig(config); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "OIDC configuration error", "error": err.Error()}) + return + } + + // Get authorization code and state from query parameters + code := c.Query("code") + state := c.Query("state") + + if code == "" { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=missing_code") + return + } + + if state == "" { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=missing_state") + return + } + + // Validate state + if !stateStore.ValidateAndRemoveState(state) { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=invalid_state") + return + } + + // Initialize OIDC provider + provider, err := oidc.NewProvider(ctx, config.OIDC.Issuer) + if err != nil { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=provider_init_failed") + return + } + + oauth2Config := createOAuth2Config(config, provider) + + // Exchange authorization code for token + token, err := oauth2Config.Exchange(ctx, code) + if err != nil { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=token_exchange_failed") + return + } + + // Extract and verify ID token + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=no_id_token") + return + } + + // Create verifier + oidcConfig := &oidc.Config{ + ClientID: config.OIDC.ClientID, + } + verifier := provider.Verifier(oidcConfig) + + // Verify ID token + idToken, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=token_verification_failed") + return + } + + // Extract claims + var claims map[string]interface{} + if err := idToken.Claims(&claims); err != nil { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=claims_extraction_failed") + return + } + + // Extract user information + sub, _ := claims["sub"].(string) + email, _ := claims["email"].(string) + name, _ := claims["name"].(string) + + if name == "" { + name, _ = claims["preferred_username"].(string) + } + if name == "" { + name = email + } + + if sub == "" || email == "" { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=missing_user_info") + return + } + + // Extract and map roles + roles := extractRolesFromClaims(claims) + icaroRole := mapRoleToIcaro(roles, config) + + // Block access if role is not authorized for OIDC login + if icaroRole == "" { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=unauthorized_role") + return + } + + // Handle account mapping based on role + db := database.Instance() + var account models.Account + + if icaroRole == "admin" { + // If user should be admin, find the primary admin account (lowest ID) + db.Where("type = ?", "admin").Order("id ASC").First(&account) + + if account.Id == 0 { + // No admin account found, create error + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=no_admin_account_found") + return + } + + // Use the existing admin account - don't modify it + // This allows super admin from Logto to act as the main admin + } else { + // For non-admin roles, look for existing account by email first, then Logto sub + db.Where("email = ?", email).First(&account) + + if account.Id == 0 { + // If not found by email, try by Logto sub + db.Where("username = ? OR uuid = ?", sub, sub).First(&account) + } + + if account.Id == 0 { + // Create new account for non-admin users if none found + account = models.Account{ + Username: sub, + Password: "", // No password for OIDC accounts + Name: name, + Email: email, + Type: icaroRole, + Uuid: sub, // Use Logto subject ID as UUID for traceability + Created: time.Now().UTC(), + } + + // Create the account + if err := db.Create(&account).Error; err != nil { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=account_creation_failed") + return + } + } + // If user exists, do not update - use existing account as is + } + + // Create authorization token for the session + h := sha256.New() + h.Write([]byte(time.Now().UTC().String() + sub + rawIDToken)) + authToken := fmt.Sprintf("%x", h.Sum(nil)) + + // Set expiration date + expires := time.Now().UTC().AddDate(0, 0, configuration.Config.TokenExpiresDays) + + accessToken := models.AccessToken{ + AccountId: account.Id, + Token: authToken, + Role: account.Type, + Type: "oidc", + Expires: expires, + ACLs: "full", + Description: "OIDC Login", + } + + db.Save(&accessToken) + + // Generate a cryptographically secure temporary one-time code (expires in 2 minutes) + codeBytes := make([]byte, 16) + if _, err := rand.Read(codeBytes); err != nil { + c.Redirect(http.StatusTemporaryRedirect, config.OIDC.FrontendURL+"/?error=code_generation_failed") + return + } + tempCode := fmt.Sprintf("%x", codeBytes) + codeExpiration := time.Now().Add(2 * time.Minute) + + // Store the code with token and account information + codeStore.StoreCode(tempCode, authToken, account.Id, codeExpiration) + + // Redirect to frontend with temporary code instead of token + callbackURL := fmt.Sprintf("%s/#/login/callback?code=%s", + config.OIDC.FrontendURL, + tempCode, + ) + + c.Redirect(http.StatusTemporaryRedirect, callbackURL) +} + +// OIDCExchange exchanges a temporary code for authentication token +func OIDCExchange(c *gin.Context) { + // Get the temporary code from request + var request struct { + Code string `json:"code" binding:"required"` + } + + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid request", "error": err.Error()}) + return + } + + // Validate and retrieve the code data + codeData, valid := codeStore.ValidateAndRemoveCode(request.Code) + if !valid { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid or expired code"}) + return + } + + // Get account information + db := database.Instance() + var account models.Account + if err := db.Where("id = ?", codeData.AccountID).First(&account).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Account not found"}) + return + } + + // Get token expiration from database + var accessToken models.AccessToken + if err := db.Where("token = ?", codeData.Token).First(&accessToken).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Token not found"}) + return + } + + // Return the token information + c.JSON(http.StatusOK, gin.H{ + "token": codeData.Token, + "expires": accessToken.Expires.Unix(), + "id": account.Id, + "account_type": account.Type, + }) +} diff --git a/sun/sun-ui/src/App.vue b/sun/sun-ui/src/App.vue index 61b0cc946..3b392ff55 100644 --- a/sun/sun-ui/src/App.vue +++ b/sun/sun-ui/src/App.vue @@ -7,7 +7,7 @@  logo
-
+ -
Copyright © {{currentYear()}} {{companyName}}
+
{{ $t("login.copyright") }} © {{currentYear()}} {{companyName}}
Privacy
@@ -335,11 +352,15 @@
+ + + +