From d76eafa4376142be700f8e39648c975a4d0c56e3 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Thu, 29 Aug 2024 16:35:20 +1000 Subject: [PATCH] fix(handlers): redirect uri scenario not handled This fixes an edge case where the access request would not return a relevant error if the access request was an OpenID Connect 1.0 request with an absent redirect URI. This technically should not be possible but it's a safeguard against the future. --- handler/oauth2/flow_authorize_code_token.go | 16 +- .../oauth2/flow_authorize_code_token_test.go | 436 ++++++++++-------- 2 files changed, 268 insertions(+), 184 deletions(-) diff --git a/handler/oauth2/flow_authorize_code_token.go b/handler/oauth2/flow_authorize_code_token.go index 43fda0ed..4ad81764 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -56,7 +56,7 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C return errorsx.WithStack(oauth2.ErrInvalidGrant.WithHint(hint).WithDebug(debug)) case errors.Is(err, oauth2.ErrNotFound): - return errorsx.WithStack(oauth2.ErrInvalidGrant.WithWrap(err).WithDebugError(err)) + return errorsx.WithStack(oauth2.ErrInvalidGrant.WithWrap(err).WithDebugf("The authorization code session for the given authorization code was not found.")) case err != nil: return errorsx.WithStack(oauth2.ErrServerError.WithWrap(err).WithDebugError(err)) } @@ -84,9 +84,17 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C // "redirect_uri" parameter was included in the initial authorization // request as described in Section 4.1.1, and if included ensure that // their values are identical. - forcedRedirectURI := authorizeRequest.GetRequestForm().Get(consts.FormParameterRedirectURI) - if forcedRedirectURI != "" && forcedRedirectURI != request.GetRequestForm().Get(consts.FormParameterRedirectURI) { - return errorsx.WithStack(oauth2.ErrInvalidGrant.WithHint("The 'redirect_uri' from this request does not match the one from the authorize request.")) + redirectURI := authorizeRequest.GetRequestForm().Get(consts.FormParameterRedirectURI) + + switch redirectURI { + case "": + if authorizeRequest.GetRequestedScopes().Has(consts.ScopeOpenID) { + return errorsx.WithStack(oauth2.ErrInvalidGrant.WithHint("The 'redirect_uri' parameter is required when using OpenID Connect 1.0.")) + } + case request.GetRequestForm().Get(consts.FormParameterRedirectURI): + break + default: + return errorsx.WithStack(oauth2.ErrInvalidGrant.WithHint("The 'redirect_uri' from this request does not match the one from the authorize request.").WithDebugf("The 'redirect_uri' parameter value '%s' utilized in the Access Request does not match the original 'redirect_uri' parameter value '%s' requested in the Authorize Request which is not permitted.", request.GetRequestForm().Get(consts.FormParameterRedirectURI), redirectURI)) } // Checking of POST client_id skipped, because: diff --git a/handler/oauth2/flow_authorize_code_token_test.go b/handler/oauth2/flow_authorize_code_token_test.go index a5e36b11..4e1810a2 100644 --- a/handler/oauth2/flow_authorize_code_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -237,204 +237,280 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { } } -func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { - for k, strategy := range map[string]CoreStrategy{ - "hmac": &hmacshaStrategy, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() +func TestAuthorizeExplicitGrantHandler_HandleTokenEndpointRequest(t *testing.T) { + strategy := &hmacshaStrategy - h := AuthorizeExplicitGrantHandler{ - CoreStorage: store, - AuthorizeCodeStrategy: &hmacshaStrategy, - TokenRevocationStorage: store, - Config: &oauth2.Config{ - ScopeStrategy: oauth2.HierarchicScopeStrategy, - AudienceMatchingStrategy: oauth2.DefaultAudienceMatchingStrategy, - AuthorizeCodeLifespan: time.Minute, + testCases := []struct { + name string + r *oauth2.AccessRequest + ar *oauth2.AuthorizeRequest + setup func(t *testing.T, s CoreStorage, r *oauth2.AccessRequest, ar *oauth2.AuthorizeRequest) + check func(t *testing.T, s CoreStorage, r *oauth2.AccessRequest, ar *oauth2.AuthorizeRequest) + expected string + }{ + { + "ShouldPassOAuth20", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, + Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, - } - for i, c := range []struct { - areq *oauth2.AccessRequest - authreq *oauth2.AuthorizeRequest - description string - setup func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) - check func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) - expectErr error - }{ - { - areq: &oauth2.AccessRequest{ - GrantTypes: oauth2.Arguments{"12345678"}, - }, - description: "should fail because not responsible", - expectErr: oauth2.ErrUnknownRequest, + }, + &oauth2.AuthorizeRequest{ + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, + Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, + Session: &oauth2.DefaultSession{}, + RequestedScope: oauth2.Arguments{"a", "b"}, + RequestedAt: time.Now().UTC(), }, - { - areq: &oauth2.AccessRequest{ - GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{""}}, - Session: &oauth2.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - description: "should fail because client is not granted this grant type", - expectErr: oauth2.ErrUnauthorizedClient, + }, + nil, + nil, + "", + }, + { + "ShouldPassOpenIDConnect", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, + Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, - { - areq: &oauth2.AccessRequest{ - GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{GrantTypes: []string{"authorization_code"}}, - Session: &oauth2.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - description: "should fail because authcode could not be retrieved (1)", - setup: func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) { - token, _, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) - require.NoError(t, err) - areq.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} - }, - expectErr: oauth2.ErrInvalidGrant, + }, + &oauth2.AuthorizeRequest{ + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, + Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, + Session: &oauth2.DefaultSession{}, + RequestedScope: oauth2.Arguments{consts.ScopeOpenID, "a", "b"}, + RequestedAt: time.Now().UTC(), }, - { - areq: &oauth2.AccessRequest{ - GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, - Request: oauth2.Request{ - Form: url.Values{consts.FormParameterAuthorizationCode: {"foo.bar"}}, - Client: &oauth2.DefaultClient{GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, - Session: &oauth2.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - description: "should fail because authcode validation failed", - expectErr: oauth2.ErrInvalidGrant, + }, + nil, + nil, + "", + }, + { + "ShouldPass", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, + Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, - { - areq: &oauth2.AccessRequest{ - GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, - Session: &oauth2.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - authreq: &oauth2.AuthorizeRequest{ - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{ID: "bar"}, - RequestedScope: oauth2.Arguments{"a", "b"}, - }, - }, - description: "should fail because client mismatch", - setup: func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) { - token, signature, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) - require.NoError(t, err) - areq.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} - - require.NoError(t, store.CreateAuthorizeCodeSession(context.TODO(), signature, authreq)) - }, - expectErr: oauth2.ErrInvalidGrant, + }, + &oauth2.AuthorizeRequest{ + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, + Session: &oauth2.DefaultSession{}, + RequestedScope: oauth2.Arguments{"openid"}, + RequestedAt: time.Now().UTC(), }, - { - areq: &oauth2.AccessRequest{ - GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, - Session: &oauth2.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - authreq: &oauth2.AuthorizeRequest{ - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, - Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, - Session: &oauth2.DefaultSession{}, - }, - }, - description: "should fail because redirect uri was set during /authorize call, but not in /token call", - setup: func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) { - token, signature, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) - require.NoError(t, err) - areq.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} + }, + func(t *testing.T, s CoreStorage, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) { + token, signature, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) + require.NoError(t, err) - require.NoError(t, store.CreateAuthorizeCodeSession(context.TODO(), signature, authreq)) - }, - expectErr: oauth2.ErrInvalidGrant, + areq.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} + require.NoError(t, s.CreateAuthorizeCodeSession(context.TODO(), signature, authreq)) + }, + nil, + "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The 'redirect_uri' parameter is required when using OpenID Connect 1.0.", + }, + { + "ShouldFailNotResponsible", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{"12345678"}, + }, + nil, + nil, + nil, + "The handler is not responsible for this request.", + }, + { + "ShouldFailNotGranted", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{""}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, - { - areq: &oauth2.AccessRequest{ - GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, - Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, - Session: &oauth2.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - authreq: &oauth2.AuthorizeRequest{ - Request: oauth2.Request{ - Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, - Session: &oauth2.DefaultSession{}, - RequestedScope: oauth2.Arguments{"a", "b"}, - RequestedAt: time.Now().UTC(), - }, - }, - description: "should pass", - setup: func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) { - token, signature, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) - require.NoError(t, err) + }, + nil, + nil, + nil, + "The client is not authorized to request a token using this method. The OAuth 2.0 Client is not allowed to use authorization grant 'authorization_code'.", + }, + { + "ShouldFailAuthCodeRetrieval", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{GrantTypes: []string{"authorization_code"}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + nil, + func(t *testing.T, s CoreStorage, r *oauth2.AccessRequest, ar *oauth2.AuthorizeRequest) { + token, _, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) + require.NoError(t, err) + r.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} + }, + nil, + "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The authorization code session for the given authorization code was not found.", + }, + { + "ShouldFailInvalidCode", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Form: url.Values{consts.FormParameterAuthorizationCode: {"foo.bar"}}, + Client: &oauth2.DefaultClient{GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + nil, + nil, + nil, + "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The authorization code session for the given authorization code was not found.", + }, + { + "ShouldFailClientIDMismatch", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + &oauth2.AuthorizeRequest{ + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "bar"}, + RequestedScope: oauth2.Arguments{"a", "b"}, + }, + }, + func(t *testing.T, s CoreStorage, r *oauth2.AccessRequest, ar *oauth2.AuthorizeRequest) { + token, signature, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) + require.NoError(t, err) + r.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} - areq.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} - require.NoError(t, store.CreateAuthorizeCodeSession(context.TODO(), signature, authreq)) - }, + require.NoError(t, s.CreateAuthorizeCodeSession(context.TODO(), signature, ar)) + }, + nil, + "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The OAuth 2.0 Client ID from this request does not match the one from the authorize request.", + }, + { + "ShouldFailRedirectURIPresentInAuthorizeRequestButMissingFromAccessRequest", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{consts.GrantTypeAuthorizationCode}, + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, - { - areq: &oauth2.AccessRequest{ + }, + &oauth2.AuthorizeRequest{ + Request: oauth2.Request{ + Client: &oauth2.DefaultClient{ID: "foo", GrantTypes: []string{consts.GrantTypeAuthorizationCode}}, + Form: url.Values{consts.FormParameterRedirectURI: []string{"request-redir"}}, + Session: &oauth2.DefaultSession{}, + }, + }, + func(t *testing.T, s CoreStorage, r *oauth2.AccessRequest, ar *oauth2.AuthorizeRequest) { + token, signature, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) + require.NoError(t, err) + r.Form = url.Values{consts.FormParameterAuthorizationCode: {token}} + + require.NoError(t, s.CreateAuthorizeCodeSession(context.TODO(), signature, ar)) + }, + nil, + "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The 'redirect_uri' from this request does not match the one from the authorize request. The 'redirect_uri' parameter value '' utilized in the Access Request does not match the original 'redirect_uri' parameter value 'request-redir' requested in the Authorize Request which is not permitted.", + }, + { + "ShouldFailCodeAlreadyUsed", + &oauth2.AccessRequest{ + GrantTypes: oauth2.Arguments{"authorization_code"}, + Request: oauth2.Request{ + Form: url.Values{}, + Client: &oauth2.DefaultClient{ GrantTypes: oauth2.Arguments{"authorization_code"}, - Request: oauth2.Request{ - Form: url.Values{}, - Client: &oauth2.DefaultClient{ - GrantTypes: oauth2.Arguments{"authorization_code"}, - }, - GrantedScope: oauth2.Arguments{"foo", consts.ScopeOffline}, - Session: &oauth2.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - check: func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) { - assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(oauth2.AccessToken)) - assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(oauth2.RefreshToken)) }, - setup: func(t *testing.T, areq *oauth2.AccessRequest, authreq *oauth2.AuthorizeRequest) { - code, sig, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) - require.NoError(t, err) - areq.Form.Add("code", code) + GrantedScope: oauth2.Arguments{"foo", consts.ScopeOffline}, + Session: &oauth2.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + nil, + func(t *testing.T, s CoreStorage, r *oauth2.AccessRequest, ar *oauth2.AuthorizeRequest) { + code, sig, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) + require.NoError(t, err) + r.Form.Add("code", code) - require.NoError(t, store.CreateAuthorizeCodeSession(context.TODO(), sig, areq)) - require.NoError(t, store.InvalidateAuthorizeCodeSession(context.TODO(), sig)) - }, - description: "should fail because code has been used already", - expectErr: oauth2.ErrInvalidGrant, + require.NoError(t, s.CreateAuthorizeCodeSession(context.TODO(), sig, r)) + require.NoError(t, s.InvalidateAuthorizeCodeSession(context.TODO(), sig)) + }, + func(t *testing.T, s CoreStorage, r *oauth2.AccessRequest, ar *oauth2.AuthorizeRequest) { + assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), r.GetSession().GetExpiresAt(oauth2.AccessToken)) + assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), r.GetSession().GetExpiresAt(oauth2.RefreshToken)) + }, + "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The authorization code has already been used.", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := storage.NewMemoryStore() + + handle := AuthorizeExplicitGrantHandler{ + CoreStorage: s, + AuthorizeCodeStrategy: strategy, + TokenRevocationStorage: s, + Config: &oauth2.Config{ + ScopeStrategy: oauth2.HierarchicScopeStrategy, + AudienceMatchingStrategy: oauth2.DefaultAudienceMatchingStrategy, + AuthorizeCodeLifespan: time.Minute, }, - } { - t.Run(fmt.Sprintf("case=%d/description=%s", i, c.description), func(t *testing.T) { - if c.setup != nil { - c.setup(t, c.areq, c.authreq) - } + } - t.Logf("Processing %+v", c.areq.Client) + if tc.ar != nil { + code, sig, err := strategy.GenerateAuthorizeCode(context.TODO(), nil) + require.NoError(t, err) - err := h.HandleTokenEndpointRequest(context.Background(), c.areq) - if c.expectErr != nil { - require.EqualError(t, err, c.expectErr.Error(), "%+v", err) - } else { - require.NoError(t, err, "%+v", err) - if c.check != nil { - c.check(t, c.areq, c.authreq) - } + if tc.r != nil { + if tc.r.Form == nil { + tc.r.Form = url.Values{} } - }) + + tc.r.Form.Add("code", code) + } + + require.NoError(t, s.CreateAuthorizeCodeSession(context.TODO(), sig, tc.ar)) + } + + if tc.setup != nil { + tc.setup(t, s, tc.r, tc.ar) + } + + err := handle.HandleTokenEndpointRequest(context.Background(), tc.r) + if tc.expected != "" { + require.EqualError(t, oauth2.ErrorToDebugRFC6749Error(err), tc.expected) + } else { + require.NoError(t, oauth2.ErrorToDebugRFC6749Error(err)) + if tc.check != nil { + tc.check(t, s, tc.r, tc.ar) + } } }) }