Ergodox accent without international deadkey
Gitflow + Maven = JgitVer

OpenApi generator client auth token management

In my team, we try to use a contract-first approach for our REST APIs. With multiple microservices, we need to pass user token when we call another service.

In this article, I will show how to easily pass this token with a generated RestTemplate client and Springboot.

SetUp

You can download the base project on github. It contains two servers.

The API

For this article I will use a simple CRUD API for ponies.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
---
openapi: "3.0.1"
info:
  title: "My Little Pony"
  description: "Friendship is magic"
  contact: {}
  version: "1.0.0"
paths:
  /ponies:
    summary: "Everything for ponies"
    get:
      summary: "List"
      operationId: "list"
      parameters:
        - name: "name"
          in: "query"
          required: false
          schema:
            type: "string"
          example: "Rainbow Dash"
      responses:
        "200":
          description: "Status 200"
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/Pony"
        "400":
          description: "Status 400"
    post:
      summary: "Create"
      operationId: "create"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pony"
      responses:
        "201":
          description: "Status 201"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pony"
        "400":
          description: "Status 400"
  /ponies/{ponyId}:
    get:
      summary: "Get one"
      operationId: "getOne"
      responses:
        "200":
          description: "Status 200"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pony"
    put:
      summary: "Update"
      operationId: "update"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pony"
      responses:
        "200":
          description: "Status 200"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pony"
    delete:
      summary: "Delete one"
      operationId: "deleteOne"
      responses:
        "200":
          description: "Status 200"
    parameters:
      - name: "ponyId"
        in: "path"
        required: true
        schema:
          type: "string"
components:
  schemas:
    Pony:
      type: "object"
      required:
        - "Color"
        - "Name"
      properties:
        Id:
          type: "string"
        Name:
          type: "string"
          minLength: 1
          maxLength: 100
          example: "Big McIntosh"
        Color:
          $ref: "#/components/schemas/Color"
        CreatedAt:
          type: "integer"
          format: "int64"
          description: "Timestamp"
    Color:
      type: "string"
      enum:
        - "RED"
        - "RAINBOW"
        - "BLUE"
      example: "RED"

Front

It is the Authorization Server. You can obtain a JWT token with this call :

1
2
3
4
5
6
curl --location --request POST 'localhost:8080/oauth/token' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --header 'Authorization: Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0' \
    --data-urlencode 'username=user1' \
    --data-urlencode 'password=password' \
    --data-urlencode 'grant_type=password'

This server will also contains the generated client.

Back

This one contains the generated server-side. All endpoints required an authenticated connexion with a bearer token generated by the front.

Client

The client is generated with java/restTemplate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${basedir}/src/main/resources/api.yml</inputSpec>
                <generatorName>java</generatorName>
                <library>resttemplate</library>
                <configOptions>
                    <sourceFolder>src/gen/java/main</sourceFolder>
                    <dateLibrary>java8</dateLibrary>
                </configOptions>
                <modelPackage>bzh.zomzog.demo.openapispringclienttoken.front.domain.api</modelPackage>
                <apiPackage>bzh.zomzog.demo.openapispringclienttoken.front.controller.api</apiPackage>
                <generateSupportingFiles>true</generateSupportingFiles>
                <generateApiTests>false</generateApiTests>
                <generateModelTests>false</generateModelTests>
            </configuration>
        </execution>
    </executions>
</plugin>

This generator creates Spring @Component for each API and a common ApiClient. This ApiClient required a RestTemplate bean for spring injection, so we will modify this part for adding authentification to external calls.

By default, spring-web provides a predefined RestTemplateBuilder so we just need to add an interceptor to it. We just need to extract the token from SecurityContextHolder and add it to the headers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
    return restTemplateBuilder.additionalInterceptors((httpRequest, bytes, clientHttpRequestExecution) -> {
        Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
        if (details instanceof OAuth2AuthenticationDetails) {
            OAuth2AuthenticationDetails oauthsDetails = (OAuth2AuthenticationDetails) details;
            String type = oauthsDetails.getTokenType();
            String token = oauthsDetails.getTokenValue();
            httpRequest.getHeaders().add(HttpHeaders.AUTHORIZATION, type + " " + token);
        } else {
            throw new RuntimeException("Not oauth2");
        }
        return clientHttpRequestExecution.execute(httpRequest, bytes);
    }).build();
}

ApiClient basePath can set with a @PostConstruct

1
2
3
4
5
6
private final ApiClient apiClient;

@PostConstruct
public void config(){
    apiClient.setBasePath("http://localhost:8081");
}

Conclusion

With this configuration, each calls with generated clients will contains the token.

If multiple clients need to be generated the ApiClient can be generated separately and mutualized.

Source code example can be found on Github


Ergodox accent without international deadkey
Gitflow + Maven = JgitVer

Share

comments powered by Disqus