spring-cloud / spring-cloud-contract

Support for Consumer Driven Contracts in Spring
https://cloud.spring.io/spring-cloud-contract
Apache License 2.0
719 stars 438 forks source link

Support for Bearer style auth/oauth tokens #230

Closed pfrank13 closed 7 years ago

pfrank13 commented 7 years ago

Currently there is no facility for supporting auth tokens that are needed to be obtained before other endpoints can be hit against a given API. This seems like something that is very common that would be useful to have top level support for in the project.

To me there are 2 large issues

  1. 3 legged oauth requiring a different host to be hit to obtain a token. AFAIK everything is tied to the API host under test within Spring Cloud Contract
  2. Persisting the obtained token before other endpoint tests can use it

Currently I have a hack that, for the most part, conquers the second item in an extreme code smelly way for the non 3 legged oauth case. Hitting the token generating endpoint in my API I extract the token and then save that out in a static member in the Base class by adding an instance method that is executed via

jsonPath('$.access_token', byCommand('saveAuthToken($it)'))

then subsequent endpoint tests retrieve that value via

header('Authorization', execute('authToken()'))
  private static String authToken;

  public void saveAuthToken(final Object authToken){
    Te2Base.authToken = (String)authToken;
  }

  public String authToken(){
    return "Bearer " + authToken;
  }

I use the scenario ordering facility to make sure the token retrieval happens first, which slaps on

@FixMethodOrder(MethodSorters.NAME_ASCENDING)

Obviously this approach is a hack and totally breaks down for people that need multiple different base classes, that's forgetting the smell of an instance member setting a static member.

cah-andrew-fitzgerald commented 7 years ago

Here's how pact-jvm handles this issue:

    @TargetRequestFilter
    public void exampleRequestFilter(HttpRequest request) {
      request.addHeader("Authorization", "OAUTH hdsagasjhgdjashgdah...");
    }

The user provides a method annotated with @TargetRequestFilter, then that method is applied to every incoming request.

https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-junit#modifying-the-requests-before-they-are-sent-version-323245

marcingrzejszczak commented 7 years ago

I want to reuse the http://wiremock.org/docs/response-templating/ Response templating approach. You'd be able to set sth like this:

Contract contractDsl = Contract.make {
                request {
                    method 'GET'
                    url '/api/v1/xxxx'
                    headers {
                        header(authorization(), "secret")
                    }
                    body(foo: "bar")
                }
                response {
                    status 200
                    headers {
                        header(authorization(), fromRequest().headers(authorization()))
                    }
                    body(
                            url: fromRequest().url(),
                            headers: fromRequest().headers(authorization())
                    )
                }
            }

I'm still prototyping the API . Cause @pfrank13 I understand that this is the problem? That you'd like to set in the response of the header the Authorization header ? Cause if this relates to the server tests and that you'd like to set some headers than you can use this (I think you can use this)

RestAssuredMockMvc.postProcessors().add(new RequestPostProcessor() {
            @Override
            MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
                // add the headers
                                return request
            }
        })

or you can add your own custom filter and do sth similar as presented above and add it to mock mvc context.

pfrank13 commented 7 years ago

My issue is all about the client side, so specifically the retrieval of the auth token being strictly before testing a given endpoint and setting it in the request, that's it. The additional curveball is the auth token may come from another host.

marcingrzejszczak commented 7 years ago

But you don't need a real token. Why not just test the integration via some fixed headers? Can you show how your client test looks like?

pfrank13 commented 7 years ago

In my particular case I'm not in control of the server side API, so I'm only really using half of Cloud Contract. I'm POCing using Spring Cloud Contract to detect contract drift with our 3rd parties, it's a problem that we have with our integrations. So the token call needs to happen because I'm using a live API.

pfrank13 commented 7 years ago

This is what I'm doing

Contract.make {
  request {
    method 'POST'
    urlPath '/v1/tokens'
    body ([name: 'example@example.com',
        nameType: 'EMAIL',
      credential: 'credential2',
      credentialType: 'CREDENTIAL']
    )
    headers {
      accept(applicationJson())
      contentType(applicationJson())
      header('Authorization', 'Basic someBasicAuth=')
    }
  }
  response {
    status 200
    body (
             [access_token:'123']
           )
    headers {
      contentType(applicationJson())
    }
    testMatchers {
      jsonPath('$.access_token', byCommand('saveAuthToken($it)'))
    }
  }
}

Contract.make {
  request {
    method 'GET'
    urlPath '/v2/menus/1000/places/1234'
    headers {
      accept(applicationJson())
      contentType(applicationJson())
      header('Authorization', 'Basic someBasicAuth=')
      header('Authorization', execute('authToken()'))
    }
  }
  ...
}
marcingrzejszczak commented 7 years ago

I don't follow. This contract is for the consumer side (since you can't control the producer side) ? Or is that a contract that simulates the producer side?

Are you using contracts to perform some end to end tests? Or are you trying to use SC-Contract's DSL to defne the API of the 3rd party?

pfrank13 commented 7 years ago

Yeah that contract is all the consumer side, I'm using the DSL to just detect drift, e.g. when the 3rd party breaks what they were previously doing. I'm using the maven plugin in explicit mode. But more abstractly having to get an auth token to hit other endpoints is a fairly common thing to have to do, so even if I was controlling the server I would presumably still have the same issue as a client, i.e. I would need to get a real token in order to hit other endpoints during a test.

marcingrzejszczak commented 7 years ago

Hmm ok so the DSL works as a scenario for your tests. Fair enough, you're not the first person who does it like this ;) I think the best thing to do would be to use the post processor

RestAssuredMockMvc.postProcessors().add(new RequestPostProcessor() {
            @Override
            MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
                // add the headers
                                return request
            }
        })

In DSL I'd put a sample value for the Authorization header but in the post processor I would do a real call to fetch the real header value and I'd amend the request to contain that value of the header. In the response for the producer side I'd check if the authorization header is present and matches some regex - I wouldn't check a concrete value. For the stub I'd put a fixed value of the header and that's it. Does it make sense?

pfrank13 commented 7 years ago

Yeah that makes sense, although in my particular case I'm using RestAssured and not RestAssuredMockMvc, I don't think RestAssured has post processors, at least as far as I know.

marcingrzejszczak commented 7 years ago

You can call RestAssured.authentication - look here - https://github.com/rest-assured/rest-assured/wiki/usage#default-values .

So actually I think we can close this issue cause there's nothing programatic to be done here right?

pfrank13 commented 7 years ago

Yeah looks that way. Thanks for the help!