github.com/thetreep/go-swagger@v0.0.0-20240223100711-35af64f14f01/examples/composed-auth/README.md (about)

     1  # Composed Security Requirements
     2  
     3  The full code of this example is [here][example_code].
     4  
     5  This sample API demonstrates how to compose several authentication schemes 
     6  and configure complex security requirements for your operations.
     7  
     8  In this example, we mix security requirements with AND and OR constraints.
     9  
    10  This API apes a very simple market place with customers and resellers of items.
    11  
    12  Personas:
    13  
    14    - as a first time user, I want to see all items on sales
    15    - as a registered customer, I want to post orders for items and 
    16      consult my past orders
    17    - as a registered reseller, I want to see all pending orders on the items 
    18      I am selling on the market place
    19    - as a reseller managing my own inventories, I want to post replenishment orders for the items I provide
    20    - as a registered user, I want to consult my personal account infos
    21  
    22  The playground situation we defined is as follows:
    23  
    24    - every known user is authenticated using a basic username:password pair
    25    - resellers are authenticated using API keys - we leave them the option to authenticate using a header or a query param
    26    - any registered user (customer or reseller) will add a signed JWT to access more API endpoints
    27  
    28  Authentication with tokens allows us to inspect the signed claims in this token.
    29  
    30  Obviously, there are several ways to achieve the same result. We just wanted to demonstrate here how
    31  security requirements may be composed out of several schemes, and use API authorizers.
    32  
    33  > Note that we used the "OAuth2" declaration here but don't actually follow an OAuth2 workflow:
    34  > our intend here is to be able to extract scopes from the claims passed in a JWT token 
    35  > (the only way to manipulate scoped authorizers with Swagger 2.0 is to declare them with type `oauth2`).
    36  
    37  
    38  ### Caveats
    39  
    40  1. There should be at most one Authorization header: mixing "Authorization Basic" and "Authorization Bearer" won't work well
    41  2. There should be at most one scoped authentication scheme: if we define several such authorizers they would all use the same bearer token
    42  3. The "OAuth2" type supports other methods than the "Authorization: Bearer" header: the token may be passed
    43     using the `access_token` query param (or urlEncoded form value)
    44  4. Unfortunately, Swagger 2.0 only supports "OAuth2" as a scoped method
    45  5. There is one single principal and several methods to define it. Getting to these different intermediary principals requires some 
    46     interaction with the http request's context (e.g. using `middleware.SecurityPrincipalFrom(req)`). This is not demonstrated here for now.
    47  
    48  ### Prerequisites
    49  
    50  `golang-jwt/jwt` ships with a nice JWT CLI utility. Although not required, you might want to install it and 
    51  play with your own tokens:
    52  
    53  - `go install github.com/golang-jwt/jwt/cmd/jwt`
    54  
    55  ### Swagger specification
    56  
    57  We defined the following security schemes (in `swagger.yml` specification document):
    58  
    59  ```yaml
    60  securityDefinitions:
    61    isRegistered:
    62      # This scheme uses the header: "Authorization: Basic {base64 encoded string defined by username:password}"
    63      # Scopes are not supported with this type of authorization.
    64      type: basic
    65    isReseller:
    66      # This scheme uses the header: "X-Custom-Key: {base64 encoded string}"
    67      # Scopes are not supported with this type of authorization.
    68      type: apiKey
    69      in: header
    70      name: X-Custom-Key
    71    isResellerQuery:
    72      # This scheme uses the query parameter "CustomKeyAsQuery"
    73      # Scopes are not supported with this type of authorization.
    74      type: apiKey
    75      in: query
    76      name: CustomKeyAsQuery
    77    hasRole:
    78      # This scheme uses the header: "Authorization: Bearer {base64 encoded string representing a JWT}"
    79      # Alternatively, the query param: "access_token" may be used.
    80      #
    81      # In our scenario, we must use the query param version in order to avoid 
    82      # passing several headers with key 'Authorization'
    83      type: oauth2
    84      # The flow and URLs in spec are for documentary purpose: go-swagger does not implement OAuth workflows
    85      flow: accessCode
    86      authorizationUrl: 'https://dummy.oauth.net/auth'
    87      tokenUrl: 'https://dumy.oauth.net/token'
    88      # Required scopes are passed by the runtime to the authorizer
    89      scopes:
    90        customer: scope of registered customers
    91        inventoryManager: scope of resellers acting as inventory managers
    92  ```
    93  
    94  We specify the following security requirements:
    95  
    96  - A default requirements for all endpoints: so by default, all endpoints use the Basic auth.
    97  
    98  ```yaml
    99  security:
   100    - isRegistered: []
   101  ```
   102  
   103  - Some endpoints are not restricted at all: this is made explicit by overriding the default security requirement with an empty array.
   104  
   105  ```yaml
   106  paths:
   107    /items:
   108      get:
   109        summary: items on sale
   110        operationId: GetItems
   111        description: |
   112          Everybody should be able to access this operation
   113        security: []
   114  ...
   115  ```
   116  
   117  - We created endpoints with various compositions of our 3 security schemes
   118  
   119  Example: `isRegistered` **AND** `hasRole[ customer ]`
   120  ```yaml
   121    /order/{orderID}:
   122      get:
   123        summary: retrieves an order
   124        operationId: GetOrder
   125        description: |
   126          Only registered customers should be able to retrieve orders
   127        security: 
   128          - isRegistered: []
   129            hasRole: [ customer ]  
   130  ...
   131  ```
   132  
   133  Example: (`isRegistered` **AND** `hasRole[ customer ]`) **OR** (`isReseller` **AND** `hasRole[ inventoryManager ]`) **OR** (`isResellerQuery` **AND** `hasRole[ inventoryManager ]`)
   134  
   135  ```yaml
   136    /order/add:
   137      post:
   138        summary: post a new order
   139        operationId: AddOrder
   140        description: |
   141          Registered customers should be able to add purchase orders.
   142          Registered inventory managers should be able to add replenishment orders.
   143  
   144        security:
   145          - isRegistered: []
   146            hasRole: [ customer ]  
   147          - isReseller: []
   148            hasRole: [ inventoryManager ]  
   149          - isResellerQuery: []
   150            hasRole: [ inventoryManager ]  
   151  ...
   152  ```
   153  
   154  Example: isReseller **OR** isResellerQuery
   155  
   156  This one allows to pass an API key either by header or by query param.
   157  
   158  ```yaml
   159    /orders/{itemID}:
   160      get:
   161        summary: retrieves all orders for an item
   162        operationId: GetOrdersForItem
   163        description: |
   164          Only registered resellers should be able to search orders for an item
   165        security:
   166          - isReseller: []
   167          - isResellerQuery: []
   168  ...
   169  ```
   170  We need to specify a security principal in the model and generate the server with this. Operations will be passed this principal as 
   171  parameter upon successful authentication.
   172  
   173  When using the scoped authentication ("oauth2"), our custom authorizer with pass all claimed roles that match the security requirement in the principal.
   174  
   175  ```yaml
   176  definitions:
   177    ...
   178    principal:
   179      type: object 
   180      properties: 
   181        name: 
   182          type: string
   183        roles:
   184          type: array 
   185          items: 
   186            type: string
   187  ```
   188  
   189  ### Generate the server 
   190  
   191  ```shell
   192  swagger generate server -A multi-auth-example -P models.Principal -f ./swagger.yml
   193  ```
   194  
   195  Files `restapi/configure_multi_auth_example.go` and `auth/authorizers.go` are not generated.
   196  
   197  ### Testing configuration
   198  
   199  #### Test tokens and keys
   200  In `./tokens`, we provided with some ready made tokens. If you have installed the `jwt` CLI, 
   201  you can play around an build some different claims as JWT (see the `make-tokens.sh` script for usage).
   202  
   203  > **NOTE:** tokens need a pair of public / private keys (for the signer and the verifier). We generated these keys 
   204  > for testing purpose in the `keys` directory (RSA256 keys).
   205  
   206  Our JWT defines "roles" as custom claim (in `auth/authorizers.go`): this means the signer of the token acknowledges the 
   207  holder of the token to be enabled for these.
   208  
   209  ```go
   210  // roleClaims describes the format of our JWT token's claims
   211  type roleClaims struct {
   212  	Roles []string `json:"roles"`
   213  	jwt.StandardClaims
   214  }
   215  ```
   216  
   217  #### Configure the API with custom authorizers
   218  
   219  In `configure_multi_auth_example.go` we have set up our custom authorizers:
   220  
   221  ```go
   222  func configureAPI(api *operations.MultiAuthExampleAPI) http.Handler {
   223  	// configure the api here
   224  	api.ServeError = errors.ServeError
   225  
   226      ...
   227  	logger := logging.MustGetLogger("api")
   228  
   229  	api.Logger = logger.Infof
   230  
   231  	api.JSONConsumer = runtime.JSONConsumer()
   232  
   233  	api.JSONProducer = runtime.JSONProducer()
   234  
   235  	// Applies when the "Authorization: Basic" header is set with the Basic scheme
   236  	api.IsRegisteredAuth = func(user string, pass string) (*models.Principal, error) {
   237  		// The header: Authorization: Basic {base64 string} has already been decoded by the runtime as a username:password pair
   238  		api.Logger("IsRegisteredAuth handler called")
   239  		return auth.IsRegistered(user, pass)
   240  	}
   241  
   242  	// Applies when the "Authorization: Bearer" header or the "access_token" query is set
   243  	api.HasRoleAuth = func(token string, scopes []string) (*models.Principal, error) {
   244  		// The header: Authorization: Bearer {base64 string} (or ?access_token={base 64 string} param) has already
   245  		// been decoded by the runtime as a token
   246  		api.Logger("HasRoleAuth handler called")
   247  		return auth.HasRole(token, scopes)
   248  	}
   249  
   250  	// Applies when the "CustomKeyAsQuery" query is set
   251  	api.IsResellerQueryAuth = func(token string) (*models.Principal, error) {
   252  		api.Logger("ResellerQueryAuth handler called")
   253  		return auth.IsReseller(token)
   254  	}
   255  
   256  	// Applies when the "X-Custom-Key" header is set
   257  	api.IsResellerAuth = func(token string) (*models.Principal, error) {
   258  		api.Logger("IsResellerAuth handler called")
   259  		return auth.IsReseller(token)
   260  	}
   261      ...
   262  ```
   263  
   264  These authorizers are implemented in `auth/authorizers.go`.
   265  
   266  Here is the basic one:
   267  ```go 
   268  // IsRegistered determines if the user is properly registered,
   269  // i.e if a valid username:password pair has been provided
   270  func IsRegistered(user, pass string) (*models.Principal, error) {
   271  	logger.Debugf("Credentials: %q:%q", user, pass)
   272  	if password, ok := userDb[user]; ok {
   273  		if pass == password {
   274  			return &models.Principal{
   275  				Name: user,
   276  			}, nil
   277  		}
   278  	}
   279  	logger.Debug("Bad credentials")
   280  	return nil, errors.New(401, "Unauthorized: not a registered user")
   281  }
   282  ```
   283  
   284  We did not set up actual operations: they are mere debug loggers, returning a "Not implemented" error.
   285  We log on the serve console how the principal is passed to the operation.
   286  
   287  ```go 
   288  	api.AddOrderHandler = operations.AddOrderHandlerFunc(func(params operations.AddOrderParams, principal *models.Principal) middleware.Responder {
   289  		logger.Warningf("AddOrder called with params: %s, and principal: %s", spew.Sdump(params.Order), spew.Sdump(principal))
   290  		return middleware.NotImplemented("operation .AddOrder has not yet been implemented")
   291  	})
   292  ```
   293  
   294  ### Run the server
   295  
   296  ```shell
   297  go run ./cmd/multi-auth-example-server/main.go --port 43016
   298  ```
   299  
   300  ### Exercise your authorizers
   301  
   302  There is a little exercising utility script: `exerciser.sh`. 
   303  This script pushes a sequence of curl requests. You may customize it to your liking and further exercise the API.
   304  
   305  Authorizations actions and operations are logged on the server console.
   306  
   307  Example:
   308  ```shell
   309  curl \
   310    --verbose \
   311    --get \
   312    --header "X-Custom-Key: `cat tokens/token-apikey-reseller.jwt`" \
   313    "http://localhost:43016/api/orders/myItem"
   314  
   315  *   Trying 127.0.0.1...
   316  * Connected to localhost (127.0.0.1) port 43016 (#0)
   317  > GET /api/orders/myItem HTTP/1.1
   318  > Host: localhost:43016
   319  > User-Agent: curl/7.47.0
   320  > Accept: */*
   321  > X-Custom-Key: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsImp0aSI6ImZyZWQiLCJyb2xlcyI6WyJyZXNlbGxlciJdfQ.gvI8J3vNaXHOFCLF46Db-9tIf9Y_4xhN5ZKd0-z7AMRyrElVjG46epT_ld5p2YRyMQNXn4LPESGWkxdJVsnZPmXYkKHBUeDSb0hj523Eue-Ayf-pwMIN4DpvcAToU0XY8srlrlLIUWINn1tOPZGtprksxMfh7TkXcWHKkI8Q0P8-3JBTkoq4HBL1DzcAwYh4EGcFcgoXMUuR_TfE3SIOjUUE5Zs3c6UswPpvZv82jAGhFIs6uJI-73BvEZ084OmI0gCJNfHEms-79nDkqh5DHf6biQsABSdBfjDLNo24nkOhlOr7IOY0LSGws9xeaM8gY58lYN3Evpia642OUxwYI55fZzku4VGm7Ia2-uK_tD8AoNLquufmPP9ROAY63cZF0wnlw_6IM1gP4LQknVWb4gcdC0j7dk4SG01u4j9OhCXy2SLqx_SI9ZM5kfgAq6kGzQULRGmBbkSCFQfEzPn5v2WzAl_XmQ7uF5KJqgjDQlbamugXlz69w5eUECRpJGNjlGxb11Q-LBKgJ9An_nOSp0p3TfIIQOXTTz5W5CzC0DRsslN50l-6z0xTwtqiy47u8JhZk-073YkDWT_NS3MEAkgb48fFwLZIlnH5bAM5kZbZ4B7fql1j_G6UGY1tcmMXhfKP6ePE0PtMPSE1U7sF-nHPE7spwD5_56BjdBQf4pM
   322  >
   323  < HTTP/1.1 501 Not Implemented
   324  < Content-Type: application/json
   325  < Date: Tue, 17 Apr 2018 17:55:45 GMT
   326  < Content-Length: 59
   327  <
   328  "operation .GetOrdersForItem has not yet been implemented"
   329  ```
   330  
   331  Another example:
   332  ```shell
   333  basic=`echo "ivan:terrible"|tr -d '\n'|base64 -i`
   334  curl \
   335    --verbose \
   336    -i \
   337    -X POST \
   338    --data '{"orderID": "myorder", "orderLines": [{"quantity": 10, "purchasedItem": "myItem"}]}' \
   339    --header "Content-Type: application/json" \
   340    --header "Authorization: Basic ${basic}" \
   341    --header "X-Custom-Key: `cat tokens/token-apikey-reseller.jwt`" \
   342    "http://localhost:43016/api/order/add?access_token=`cat tokens/token-bearer-inventory-manager.jwt`"
   343  
   344  *   Trying 127.0.0.1...
   345  * Connected to localhost (127.0.0.1) port 43016 (#0)
   346  > POST /api/order/add?access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsImp0aSI6ImZyZWQiLCJyb2xlcyI6WyJpbnZlbnRvcnlNYW5hZ2VyIl19.LaWEWhrDdrcwIatu7dVk-jhmzI4RlGgV0OFa1kLY2C6qMKQDibIActx1SVYuCxLLOafycbXlzCBGddoaHUHwjtuoOgftog2jHAR8-JJYyzSHCuz68cEngRtvY-MKgGApJqYInRhhdbV-DUiadPJjryxf9NNbyrdHjSMhSJOVDQp9Rj9VEGoK0zoufKOy_YrQEfcWl8OHHS17H7CI5_L44MsyC2Z6U3-HGo2eBdoIIVe5dUINA_PZ-U6netGOYuQ_T4GJ9IYjdkUOOpd_LJrFhCCE7vs4QnxVgnhSBzu5mL_ygJMyoA0yEvP01wSBxZIgFqJtiNXB5LYf-O_T2UuPLLfHiLRkmAJMDUxH_WYkat_qBILpzuRLFjGHrM3beouwVoHvdd7P3EYkr3vhElHs3GDKC9VVnqFL2a5hQOHdgJ4w6CVesNZGZkL-ZZKNzqeTzWch2WQcrYO26sG_XsS8Y1_mZ1UkMc8aJHGaQwnDt7SXUBTgvn57XuH_Ny6S2NfHJ6TX_rPanDlMQASOLR_yAYMJsqZjZlh9qXLR1bWjv7SQ4uuKb7YTf_gcYA3axEmYSXWZZA34gG3Q6eCDjPhEdN-RPj7C8zPiIa7VUG5ay4yp8A_hTtjsWKjvC7Kh3jZpyaF3M2QcBWkwQyFxljrMyxyHdAujkh7M-Y70O9U_YLU HTTP/1.1
   347  > Host: localhost:43016
   348  > User-Agent: curl/7.47.0
   349  > Accept: */*
   350  > Content-Type: application/json
   351  > Authorization: Basic aXZhbjp0ZXJyaWJsZQ==
   352  > X-Custom-Key: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsImp0aSI6ImZyZWQiLCJyb2xlcyI6WyJyZXNlbGxlciJdfQ.gvI8J3vNaXHOFCLF46Db-9tIf9Y_4xhN5ZKd0-z7AMRyrElVjG46epT_ld5p2YRyMQNXn4LPESGWkxdJVsnZPmXYkKHBUeDSb0hj523Eue-Ayf-pwMIN4DpvcAToU0XY8srlrlLIUWINn1tOPZGtprksxMfh7TkXcWHKkI8Q0P8-3JBTkoq4HBL1DzcAwYh4EGcFcgoXMUuR_TfE3SIOjUUE5Zs3c6UswPpvZv82jAGhFIs6uJI-73BvEZ084OmI0gCJNfHEms-79nDkqh5DHf6biQsABSdBfjDLNo24nkOhlOr7IOY0LSGws9xeaM8gY58lYN3Evpia642OUxwYI55fZzku4VGm7Ia2-uK_tD8AoNLquufmPP9ROAY63cZF0wnlw_6IM1gP4LQknVWb4gcdC0j7dk4SG01u4j9OhCXy2SLqx_SI9ZM5kfgAq6kGzQULRGmBbkSCFQfEzPn5v2WzAl_XmQ7uF5KJqgjDQlbamugXlz69w5eUECRpJGNjlGxb11Q-LBKgJ9An_nOSp0p3TfIIQOXTTz5W5CzC0DRsslN50l-6z0xTwtqiy47u8JhZk-073YkDWT_NS3MEAkgb48fFwLZIlnH5bAM5kZbZ4B7fql1j_G6UGY1tcmMXhfKP6ePE0PtMPSE1U7sF-nHPE7spwD5_56BjdBQf4pM
   353  > Content-Length: 83
   354  >
   355  * upload completely sent off: 83 out of 83 bytes
   356  < HTTP/1.1 501 Not Implemented
   357  HTTP/1.1 501 Not Implemented
   358  < Content-Type: application/json
   359  Content-Type: application/json
   360  < Date: Tue, 17 Apr 2018 17:55:45 GMT
   361  Date: Tue, 17 Apr 2018 17:55:45 GMT
   362  < Content-Length: 51
   363  Content-Length: 51
   364  <
   365  "operation .AddOrder has not yet been implemented"
   366  ```
   367  
   368  [example_code]: https://github.com/thetreep/go-swagger/blob/master/examples/composed-auth/