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 @@