github.com/circl-dev/go-swagger@v0.31.0/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 `jwt-go` 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/dgrijalva/jwt-go/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/circl-dev/go-swagger/blob/master/examples/composed-auth/