authenticated-api/

directory
v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 1, 2024 License: Apache-2.0

README

Authenticated API Example

This little server is an example demonstrating how JWT's can be handled somewhat automatically with per-path validation of scopes.

We use similar code in production at DeepMap, and this example shows how authenticated API's can be written, as well as unit tested.

Some parts of this code can be generalized in the future, but for now, feel free to copy/paste and modify this example code.

API Overview

We define a trivial server which allows for creating and listing Things, please see api.yaml.

In #components/securitySchemes we define a scheme named BearerAuth:

BearerAuth:
  type: http
  scheme: bearer
  bearerFormat: JWT

This security scheme is a global requirement for all API endpoints:

security:
  - BearerAuth: [ ]

This means that all API endpoints require a JWT bearer token for access, and without any specific scopes, as denoted by [].

However, we want our addThing operation to require a special write permission, denoted by things:w. This is a convention that we use in naming scopes, noun followed by type of access, in this case, :w means write. Read permission is implicit from having a valid JWT.

To specialize the addThing handler, we override the global security with local security on POST /things:

security:
  - BearerAuth:
    - "things:w"

Implementation

Security is tricky, so we need to leverage well tested libraries for doing validation instead of implementing too much ourselves. We've chosen to use the excellent github.com/lestrrat-go/jwx library for JWT validation, and the kin-openapi request filter to help us perform validation.

First, we need to configure our OapiRequestValidator to perform authentication:

validator := middleware.OapiRequestValidatorWithOptions(spec,
    &middleware.Options{
        Options: openapi3filter.Options{
            AuthenticationFunc: NewAuthenticator(v),
        },
    })

Whenever a request comes in, openapi3filter will call the AuthenticationFunc to tell it whether the request is valid. Please see the Authenticate function in server/jwt_authenticator.go for an example how to do this.

In this example, we set up several components:

  1. We create a FakeAuthenticator. This is a little helper which uses an ECDSA key to sign JWT's via the lestrrat-go/jwx tools. In a normal application deployment, you would be using an identity provider, say, Google, Auth0, AWS Cognito, etc, to give you JWT's via some auth protocol, but we wanted to keep it simple. You would never have key material present like this inside your code in a real application.
  2. The JWT validation part in our fake authenticator is reasonably thorough, and can be used as an example for production code. When an authorization header comes in, bearing an encoded JWT Token (called JWS), we validate that it was signed by the public key of our IDP. In the real world, this would be a service, in this example, this is our FakeAuthenticator
  3. Once the JWT is a legitimate one, signed by the expected authority, we start looking at claims. JWT's are very freeform, so you can put in whatever payload you like, since the implementation is up to you. As you see in the spec section above, we created a the scope things:w to denote permission to write Things. What we expect is that the JWT contains a claim named perms, that's an array of permissions granted, and that it contains the string things:w. The Authenticate function does this check.

Example

You can run the example Echo server like so:

$ go run ./examples/authenticated-api/echo/main.go
2021/10/07 14:32:45 Reader token eyJhbGciOiJFUzI1NiIsImtpZCI6ImZha2Uta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOlsiZXhhbXBsZS11c2VycyJdLCJpc3MiOiJmYWtlLWlzc3VlciIsInBlcm0iOltdfQ.Hf9dCNJLa0HQfbtJi7ndASbkTfrLc6bZBJK8HaPqtzXiDkTH6sMRoiNhf6Kb1g6z3R1tN3XEpXsghxlMRO3OLA
2021/10/07 14:32:45 Writer token eyJhbGciOiJFUzI1NiIsImtpZCI6ImZha2Uta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOlsiZXhhbXBsZS11c2VycyJdLCJpc3MiOiJmYWtlLWlzc3VlciIsInBlcm0iOlsidGhpbmdzOnciXX0.CbPT1hzWmyTt0lTyv-fiyUlnY1SGa0vrX52yFjeigx2PA1-78LVH0z5hukPKkLMPDMXL9AJrtNp0elWSD_qrBw

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.2.1
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\

It prints out two tokens for you to use in http requests. Let's assign these to some environment variables for convenience.

export RJWT=eyJhbGciOiJFUzI1NiIsImtpZCI6ImZha2Uta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOlsiZXhhbXBsZS11c2VycyJdLCJpc3MiOiJmYWtlLWlzc3VlciIsInBlcm0iOltdfQ.Hf9dCNJLa0HQfbtJi7ndASbkTfrLc6bZBJK8HaPqtzXiDkTH6sMRoiNhf6Kb1g6z3R1tN3XEpXsghxlMRO3OLA
export WJWT=eyJhbGciOiJFUzI1NiIsImtpZCI6ImZha2Uta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOlsiZXhhbXBsZS11c2VycyJdLCJpc3MiOiJmYWtlLWlzc3VlciIsInBlcm0iOlsidGhpbmdzOnciXX0.CbPT1hzWmyTt0lTyv-fiyUlnY1SGa0vrX52yFjeigx2PA1-78LVH0z5hukPKkLMPDMXL9AJrtNp0elWSD_qrBw

Let's see how this works in practice. My example commands are using HTTPie, which I find easier to use in a shell than curl:

Unauthenticated requests fail:

$ http http://localhost:8080/things
HTTP/1.1 403 Forbidden
Content-Length: 43
Content-Type: application/json; charset=UTF-8
Date: Thu, 07 Oct 2021 22:00:32 GMT

{
    "message": "Security requirements failed"
}

$ http POST http://localhost:8080/things name=SomeThing
HTTP/1.1 403 Forbidden
Content-Length: 43
Content-Type: application/json; charset=UTF-8
Date: Thu, 07 Oct 2021 22:01:11 GMT

{
    "message": "Security requirements failed"
}

Using the Writer JWT, we can insert a Thing into the server:

$ http POST http://localhost:8080/things name=SomeThing Authorization:"Bearer $WJWT"
HTTP/1.1 201 Created
Content-Length: 28
Content-Type: application/json; charset=UTF-8
Date: Thu, 07 Oct 2021 22:02:05 GMT

{
    "id": 0,
    "name": "SomeThing"
}

However, we can not insert a Thing using the reader JWT:

$ http POST http://localhost:8080/things name=SomeThing2 Authorization:"Bearer $RJWT"
HTTP/1.1 403 Forbidden
Content-Length: 43
Content-Type: application/json; charset=UTF-8
Date: Thu, 07 Oct 2021 22:02:39 GMT

{
    "message": "Security requirements failed"
}

Both JWT's, however, permit listing Things:

$ http http://localhost:8080/things Authorization:"Bearer $RJWT"
HTTP/1.1 200 OK
Content-Length: 30
Content-Type: application/json; charset=UTF-8
Date: Thu, 07 Oct 2021 22:03:12 GMT

[
    {
        "id": 0,
        "name": "SomeThing"
    }
]

$ http http://localhost:8080/things Authorization:"Bearer $WJWT"
HTTP/1.1 200 OK
Content-Length: 30
Content-Type: application/json; charset=UTF-8
Date: Thu, 07 Oct 2021 22:03:34 GMT

[
    {
        "id": 0,
        "name": "SomeThing"
    }
]

Directories

Path Synopsis
api
Package api provides primitives to interact with the openapi HTTP API.
Package api provides primitives to interact with the openapi HTTP API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL