diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index 1a03155cb2..fbb71f02c1 100644 --- a/oauth2/oauth2_device_code_test.go +++ b/oauth2/oauth2_device_code_test.go @@ -777,6 +777,73 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { require.NoError(t, err) require.Equal(t, loginFlowResp2.StatusCode, http.StatusBadRequest) }) + t.Run("case=cannot reuse device_challenge", func(t *testing.T) { + var deviceChallenge string + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + if deviceChallenge == "" { + deviceChallenge = r.URL.Query().Get("device_challenge") + } + v, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(deviceChallenge). + AcceptDeviceUserCodeRequest(payload). + Execute() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + }, + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + + hc := testhelpers.NewEmptyJarClient(t) + loginFlowResp := acceptUserCode(t, conf, hc, resp) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), loginFlowResp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, loginFlowResp.Request.URL.Query().Get("client_id"), conf.ClientID) + + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + introspectAccessToken(t, conf, token, subject) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + + resp2, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp2.DeviceCode) + require.NotEmpty(t, resp2.UserCode) + + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &resp2.UserCode, + } + + acceptResp, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(deviceChallenge). + AcceptDeviceUserCodeRequest(payload). + Execute() + + loginFlowResp2, err := hc.Get(acceptResp.RedirectTo) + require.NoError(t, err) + require.Equal(t, http.StatusForbidden, loginFlowResp2.StatusCode) + }) } func newDeviceClient( diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql index 2739ecc7d6..447f68372c 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -40,7 +40,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql index 90f0aaaede..81267a6243 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -40,7 +40,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql index 1631bd3ca2..ea274805ac 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -40,7 +40,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql index 47b8f54061..fa9cc10a52 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -35,7 +35,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL;