diff --git a/.github/workflows/artifacts.yaml b/.github/workflows/artifacts.yaml index b9ed33fc..fdd4e88f 100644 --- a/.github/workflows/artifacts.yaml +++ b/.github/workflows/artifacts.yaml @@ -61,7 +61,7 @@ jobs: uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - name: Set up Syft - uses: anchore/sbom-action/download-syft@1ca97d9028b51809cf6d3c934c3e160716e1b605 # v0.17.5 + uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8 - name: Install cosign uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 @@ -192,12 +192,38 @@ jobs: push-to-registry: true if: inputs.publish + ## Use cache for the trivy-db to avoid the TOOMANYREQUESTS error https://github.com/aquasecurity/trivy-action/pull/397 + ## To avoid the trivy-db becoming outdated, we save the cache for one day + - name: Get data + id: date + run: echo "date=$(date +%Y-%m-%d)" >> $GITHUB_OUTPUT + + - name: Restore trivy cache + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + with: + path: cache/db + key: trivy-cache-${{ steps.date.outputs.date }} + restore-keys: + trivy-cache- + - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0 with: input: image format: sarif output: trivy-results.sarif + scan-type: 'fs' + scan-ref: '.' + cache-dir: "./cache" + env: + TRIVY_SKIP_DB_UPDATE: true + TRIVY_SKIP_JAVA_DB_UPDATE: true + + ## Trivy-db uses `0600` permissions. + ## But `action/cache` use `runner` user by default + ## So we need to change the permissions before caching the database. + - name: change permissions for trivy.db + run: sudo chmod 0644 ./cache/db/trivy.db - name: Upload Trivy scan results as artifact uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 215cdf01..c20f31b9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -69,7 +69,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version: "1.21" @@ -140,7 +140,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version: "1.21" @@ -175,4 +175,4 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Dependency Review - uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5 + uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0 diff --git a/.github/workflows/trivydb-cache.yaml b/.github/workflows/trivydb-cache.yaml new file mode 100644 index 00000000..e99b4170 --- /dev/null +++ b/.github/workflows/trivydb-cache.yaml @@ -0,0 +1,39 @@ +# Note: This workflow only updates the cache. You should create a separate workflow for your actual Trivy scans. +# In your scan workflow, set TRIVY_SKIP_DB_UPDATE=true and TRIVY_SKIP_JAVA_DB_UPDATE=true. +name: Update Trivy Cache + +on: + schedule: + - cron: '0 0 * * *' # Run daily at midnight UTC + workflow_dispatch: # Allow manual triggering + +jobs: + update-trivy-db: + runs-on: ubuntu-latest + steps: + - name: Setup oras + uses: oras-project/setup-oras@9c92598691bfef1424de2f8fae81941568f5889c # v1.2.1 + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Download and extract the vulnerability DB + run: | + mkdir -p $GITHUB_WORKSPACE/.cache/trivy/db + oras pull ghcr.io/aquasecurity/trivy-db:2 + tar -xzf db.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/db + rm db.tar.gz + + - name: Download and extract the Java DB + run: | + mkdir -p $GITHUB_WORKSPACE/.cache/trivy/java-db + oras pull ghcr.io/aquasecurity/trivy-java-db:1 + tar -xzf javadb.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/java-db + rm javadb.tar.gz + + - name: Cache DBs + uses: actions/cache/save@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + with: + path: ${{ github.workspace }}/.cache/trivy + key: cache-trivy-${{ steps.date.outputs.date }} diff --git a/Dockerfile b/Dockerfile index a4039a18..9395153f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ ARG BASE_IMAGE=alpine -FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0@sha256:0c6a569797744e45955f39d4f7538ac344bfb7ebf0a54006a0a4297b153ccf0f AS xx +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx -FROM --platform=$BUILDPLATFORM golang:1.23.2-alpine3.20@sha256:9dd2625a1ff2859b8d8b01d8f7822c0f528942fe56cfe7a1e7c38d3b8d72d679 AS builder +FROM --platform=$BUILDPLATFORM golang:1.23.3-alpine3.20@sha256:c694a4d291a13a9f9d94933395673494fc2cc9d4777b85df3a7e70b3492d3574 AS builder COPY --from=xx / / @@ -35,13 +35,13 @@ RUN make release-binary RUN xx-verify /go/bin/dex && xx-verify /go/bin/docker-entrypoint -FROM alpine:3.20.3@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d AS stager +FROM alpine:3.20.3@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a AS stager RUN mkdir -p /var/dex RUN mkdir -p /etc/dex COPY config.docker.yaml /etc/dex/ -FROM alpine:3.20.3@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d AS gomplate +FROM alpine:3.20.3@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a AS gomplate ARG TARGETOS ARG TARGETARCH @@ -54,8 +54,8 @@ RUN wget -O /usr/local/bin/gomplate \ && chmod +x /usr/local/bin/gomplate # For Dependabot to detect base image versions -FROM alpine:3.20.3@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d AS alpine -FROM gcr.io/distroless/static-debian12:nonroot@sha256:26f9b99f2463f55f20db19feb4d96eb88b056e0f1be7016bb9296a464a89d772 AS distroless +FROM alpine:3.20.3@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a AS alpine +FROM gcr.io/distroless/static-debian12:nonroot@sha256:d71f4b239be2d412017b798a0a401c44c3049a3ca454838473a4c32ed076bfea AS distroless FROM $BASE_IMAGE diff --git a/README.md b/README.md index 2894dcdd..dac886ee 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Dex implements the following connectors: | [AuthProxy](https://dexidp.io/docs/connectors/authproxy/) | no | yes | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. | | [Bitbucket Cloud](https://dexidp.io/docs/connectors/bitbucketcloud/) | yes | yes | no | alpha | | | [OpenShift](https://dexidp.io/docs/connectors/openshift/) | yes | yes | no | alpha | | -| [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | +| [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassian-crowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | beta | | | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | | diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index 7d0cacb0..1ea0c1fc 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -23,7 +23,12 @@ import ( // Config holds configuration options for OpenID Connect logins. type Config struct { - Issuer string `json:"issuer"` + Issuer string `json:"issuer"` + // Some offspec providers like Azure, Oracle IDCS have oidc discovery url + // different from issuer url which causes issuerValidation to fail + // IssuerAlias provides a way to override the Issuer url + // from the .well-known/openid-configuration issuer + IssuerAlias string `json:"issuerAlias"` ClientID string `json:"clientID"` ClientSecret string `json:"clientSecret"` RedirectURI string `json:"redirectURI"` @@ -226,7 +231,9 @@ func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, bgctx, cancel := context.WithCancel(context.Background()) ctx := context.WithValue(bgctx, oauth2.HTTPClient, httpClient) - + if c.IssuerAlias != "" { + ctx = oidc.InsecureIssuerURLContext(ctx, c.IssuerAlias) + } provider, err := getProvider(ctx, c.Issuer, c.ProviderDiscoveryOverrides) if err != nil { cancel() @@ -540,6 +547,13 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I continue } groups = append(groups, s) + } else if groupMap, ok := v.(map[string]interface{}); ok { + if s, ok := groupMap["name"].(string); ok { + if c.groupsFilter != nil && !c.groupsFilter.MatchString(s) { + continue + } + groups = append(groups, s) + } } else { return identity, fmt.Errorf("malformed \"%v\" claim", groupsKey) } diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go index 66b35c3f..e31d4e0b 100644 --- a/connector/oidc/oidc_test.go +++ b/connector/oidc/oidc_test.go @@ -292,6 +292,38 @@ func TestHandleCallback(t *testing.T) { "email_verified": true, }, }, + { + name: "singularGroupResponseAsMap", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []map[string]string{{"name": "group1"}}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "multipleGroupResponseAsMap", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []map[string]string{{"name": "group1"}, {"name": "group2"}}, + "email": "emailvalue", + "email_verified": true, + }, + }, { name: "newGroupFromClaims", userIDKey: "", // not configured @@ -382,6 +414,23 @@ func TestHandleCallback(t *testing.T) { "email_verified": true, }, }, + { + name: "filterGroupClaimsMap", + userIDKey: "", // not configured + userNameKey: "", // not configured + groupsRegex: `^.*\d$`, + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []map[string]string{{"name": "group1"}, {"name": "group2"}, {"name": "groupA"}, {"name": "groupB"}}, + "email": "emailvalue", + "email_verified": true, + }, + }, } for _, tc := range tests { diff --git a/examples/example-app/templates.go b/examples/example-app/templates.go index a9425ead..7107eb87 100644 --- a/examples/example-app/templates.go +++ b/examples/example-app/templates.go @@ -6,38 +6,225 @@ import ( "net/http" ) +const css = ` + body { + font-family: Arial, sans-serif; + background-color: #f2f2f2; + margin: 0; + } + + .header { + text-align: center; + margin-bottom: 20px; + } + + .dex { + font-size: 2em; + font-weight: bold; + color: #3F9FD8; /* Main color */ + } + + .example-app { + font-size: 1em; + color: #EF4B5C; /* Secondary color */ + } + + .form-instructions { + text-align: center; + margin-bottom: 15px; + font-size: 1em; + color: #555; + } + + hr { + border: none; + border-top: 1px solid #ccc; + margin-top: 10px; + margin-bottom: 20px; + } + + label { + flex: 1; + font-weight: bold; + color: #333; + } + + p { + margin-bottom: 15px; + display: flex; + align-items: center; + } + + input[type="text"] { + flex: 2; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + outline: none; + } + + input[type="checkbox"] { + margin-left: 10px; + transform: scale(1.2); + } + + .back-button { + display: inline-block; + padding: 8px 16px; + background-color: #EF4B5C; /* Secondary color */ + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: fixed; + right: 20px; + bottom: 20px; + } + + .back-button:hover { + background-color: #C43B4B; /* Darker shade of secondary color */ + } + + .token-block { + background-color: #fff; + padding: 10px 15px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 15px; + word-wrap: break-word; + display: flex; + flex-direction: column; + gap: 5px; + position: relative; + } + + .token-title { + font-weight: bold; + display: flex; + justify-content: space-between; + align-items: center; + } + + .token-title a { + font-size: 0.9em; + text-decoration: none; + color: #3F9FD8; /* Main color */ + } + + .token-title a:hover { + text-decoration: underline; + } + + .token-code { + overflow-wrap: break-word; + word-break: break-all; + white-space: normal; + } + + pre { + white-space: pre-wrap; + background-color: #f9f9f9; + padding: 8px; + border-radius: 4px; + border: 1px solid #ddd; + margin: 0; + font-family: 'Courier New', Courier, monospace; + overflow-x: auto; + font-size: 0.9em; + position: relative; + margin-top: 5px; + } + + pre .key { + color: #c00; + } + + pre .string { + color: #080; + } + + pre .number { + color: #00f; + } +` + var indexTmpl = template.Must(template.New("index.html").Parse(` - + + + + + + Example App - Login - - + + +
+
Dex
+
Example App
+
-

- - -

-

- - -

-

- - -

-

- - -

-

- -

+
+ If needed, customize your login settings below, then click Login to proceed. +
+
+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ +

- + `)) func renderIndex(w http.ResponseWriter) { @@ -53,30 +240,116 @@ type tokenTmplData struct { } var tokenTmpl = template.Must(template.New("token.html").Parse(` - + + + + + + Tokens - - -

ID Token:

{{ .IDToken }}

-

Access Token:

{{ .AccessToken }}

-

Claims:

{{ .Claims }}

- {{ if .RefreshToken }} -

Refresh Token:

{{ .RefreshToken }}

-
- - -
- {{ end }} - + + + {{ if .IDToken }} +
+
+ ID Token: + Decode on jwt.io +
+
{{ .IDToken }}
+
+ {{ end }} + + {{ if .AccessToken }} +
+
+ Access Token: + Decode on jwt.io +
+
{{ .AccessToken }}
+
+ {{ end }} + + {{ if .Claims }} +
+
Claims:
+
{{ .Claims }}
+
+ {{ end }} + + {{ if .RefreshToken }} +
+
Refresh Token:
+
{{ .RefreshToken }}
+
+ + +
+
+ {{ end }} + + Back to Home + + + `)) diff --git a/examples/go.mod b/examples/go.mod index c2b28e9e..882c6aed 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -6,7 +6,7 @@ require ( github.com/coreos/go-oidc/v3 v3.11.0 github.com/dexidp/dex/api/v2 v2.2.0 github.com/spf13/cobra v1.8.1 - golang.org/x/oauth2 v0.23.0 + golang.org/x/oauth2 v0.24.0 google.golang.org/grpc v1.67.1 ) diff --git a/examples/go.sum b/examples/go.sum index 0bcdb008..a3240994 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -24,8 +24,8 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= diff --git a/go.mod b/go.mod index fa72edcc..dfa9e393 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/beevik/etree v1.4.0 github.com/coreos/go-oidc/v3 v3.11.0 github.com/dexidp/dex/api/v2 v2.1.0 - github.com/fsnotify/fsnotify v1.7.0 + github.com/fsnotify/fsnotify v1.8.0 github.com/ghodss/yaml v1.0.0 github.com/go-jose/go-jose/v4 v4.0.4 github.com/go-ldap/ldap/v3 v3.4.8 diff --git a/go.sum b/go.sum index 9f252270..e7a0ec0c 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= diff --git a/server/api.go b/server/api.go index 4b210df3..4454c3ca 100644 --- a/server/api.go +++ b/server/api.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log/slog" + "strconv" "golang.org/x/crypto/bcrypt" @@ -430,10 +431,11 @@ func (d dexAPI) CreateConnector(ctx context.Context, req *api.CreateConnectorReq } c := storage.Connector{ - ID: req.Connector.Id, - Name: req.Connector.Name, - Type: req.Connector.Type, - Config: req.Connector.Config, + ID: req.Connector.Id, + Name: req.Connector.Name, + Type: req.Connector.Type, + ResourceVersion: "1", + Config: req.Connector.Config, } if err := d.s.CreateConnector(ctx, c); err != nil { if err == storage.ErrAlreadyExists { @@ -446,7 +448,7 @@ func (d dexAPI) CreateConnector(ctx context.Context, req *api.CreateConnectorReq return &api.CreateConnectorResp{}, nil } -func (d dexAPI) UpdateConnector(ctx context.Context, req *api.UpdateConnectorReq) (*api.UpdateConnectorResp, error) { +func (d dexAPI) UpdateConnector(_ context.Context, req *api.UpdateConnectorReq) (*api.UpdateConnectorResp, error) { if !featureflags.APIConnectorsCRUD.Enabled() { return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APIConnectorsCRUD.Name) } @@ -476,6 +478,10 @@ func (d dexAPI) UpdateConnector(ctx context.Context, req *api.UpdateConnectorReq old.Config = req.NewConfig } + if rev, err := strconv.Atoi(defaultTo(old.ResourceVersion, "0")); err == nil { + old.ResourceVersion = strconv.Itoa(rev + 1) + } + return old, nil } @@ -536,3 +542,11 @@ func (d dexAPI) ListConnectors(ctx context.Context, req *api.ListConnectorReq) ( Connectors: connectors, }, nil } + +func defaultTo[T comparable](v, def T) T { + var zeroT T + if v == zeroT { + return def + } + return v +} diff --git a/server/handlers.go b/server/handlers.go index 6521bf6a..5954820c 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -534,23 +534,6 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity, "connector_id", authReq.ConnectorID, "username", claims.Username, "preferred_username", claims.PreferredUsername, "email", email, "groups", claims.Groups) - // we can skip the redirect to /approval and go ahead and send code if it's not required - if s.skipApproval && !authReq.ForceApprovalPrompt { - return "", true, nil - } - - // an HMAC is used here to ensure that the request ID is unpredictable, ensuring that an attacker who intercepted the original - // flow would be unable to poll for the result at the /approval endpoint - h := hmac.New(sha256.New, authReq.HMACKey) - h.Write([]byte(authReq.ID)) - mac := h.Sum(nil) - - returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID + "&hmac=" + base64.RawURLEncoding.EncodeToString(mac) - _, ok := conn.(connector.RefreshConnector) - if !ok { - return returnURL, false, nil - } - offlineAccessRequested := false for _, scope := range authReq.Scopes { if scope == scopeOfflineAccess { @@ -558,45 +541,55 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity, break } } - if !offlineAccessRequested { - return returnURL, false, nil - } + _, canRefresh := conn.(connector.RefreshConnector) - // Try to retrieve an existing OfflineSession object for the corresponding user. - session, err := s.storage.GetOfflineSessions(identity.UserID, authReq.ConnectorID) - if err != nil { - if err != storage.ErrNotFound { - s.logger.ErrorContext(ctx, "failed to get offline session", "err", err) - return "", false, err - } - offlineSessions := storage.OfflineSessions{ - UserID: identity.UserID, - ConnID: authReq.ConnectorID, - Refresh: make(map[string]*storage.RefreshTokenRef), - ConnectorData: identity.ConnectorData, - } + if offlineAccessRequested && canRefresh { + // Try to retrieve an existing OfflineSession object for the corresponding user. + session, err := s.storage.GetOfflineSessions(identity.UserID, authReq.ConnectorID) + switch { + case err != nil && err == storage.ErrNotFound: + offlineSessions := storage.OfflineSessions{ + UserID: identity.UserID, + ConnID: authReq.ConnectorID, + Refresh: make(map[string]*storage.RefreshTokenRef), + ConnectorData: identity.ConnectorData, + } - // Create a new OfflineSession object for the user and add a reference object for - // the newly received refreshtoken. - if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil { - s.logger.ErrorContext(ctx, "failed to create offline session", "err", err) + // Create a new OfflineSession object for the user and add a reference object for + // the newly received refreshtoken. + if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil { + s.logger.ErrorContext(ctx, "failed to create offline session", "err", err) + return "", false, err + } + case err == nil: + // Update existing OfflineSession obj with new RefreshTokenRef. + if err := s.storage.UpdateOfflineSessions(session.UserID, session.ConnID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) { + if len(identity.ConnectorData) > 0 { + old.ConnectorData = identity.ConnectorData + } + return old, nil + }); err != nil { + s.logger.ErrorContext(ctx, "failed to update offline session", "err", err) + return "", false, err + } + default: + s.logger.ErrorContext(ctx, "failed to get offline session", "err", err) return "", false, err } - - return returnURL, false, nil } - // Update existing OfflineSession obj with new RefreshTokenRef. - if err := s.storage.UpdateOfflineSessions(session.UserID, session.ConnID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) { - if len(identity.ConnectorData) > 0 { - old.ConnectorData = identity.ConnectorData - } - return old, nil - }); err != nil { - s.logger.ErrorContext(ctx, "failed to update offline session", "err", err) - return "", false, err + // we can skip the redirect to /approval and go ahead and send code if it's not required + if s.skipApproval && !authReq.ForceApprovalPrompt { + return "", true, nil } + // an HMAC is used here to ensure that the request ID is unpredictable, ensuring that an attacker who intercepted the original + // flow would be unable to poll for the result at the /approval endpoint + h := hmac.New(sha256.New, authReq.HMACKey) + h.Write([]byte(authReq.ID)) + mac := h.Sum(nil) + + returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID + "&hmac=" + base64.RawURLEncoding.EncodeToString(mac) return returnURL, false, nil } diff --git a/server/handlers_test.go b/server/handlers_test.go index d32101b1..08b02f75 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -519,7 +519,7 @@ func TestHandlePasswordLoginWithSkipApproval(t *testing.T) { Scopes: []string{"offline_access"}, }, expectedRes: "/auth/mockPw/cb", - offlineSessionCreated: false, + offlineSessionCreated: true, }, }