github.com/circl-dev/go-swagger@v0.31.0/examples/oauth2/README.md (about) 1 # Oauth2 Authentication sample: AccessCode workflow 2 3 The full code of this example is [here][example_code]. 4 5 This example illustrates a complete OAuth2 handshake. 6 7 We want to implement a simple access control based on a user's Google account (i.e. OpenID). 8 9 Personas: 10 11 - the user logs in on its Google account, which returns an access token that we will use 12 with our API. This mechanism follows the 'accessCode' OAuth2 workflow. 13 14 15 ### Swagger specification 16 17 Given the following security definitions (in `swagger.yml` specification document): 18 19 ```yaml 20 securityDefinitions: 21 OauthSecurity: 22 type: oauth2 23 flow: accessCode 24 authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth' 25 tokenUrl: 'https://www.googleapis.com/oauth2/v4/token' 26 scopes: 27 admin: Admin scope 28 user: User scope 29 ``` 30 31 We specify the following security requirements: 32 33 - A default requirements for all endpoints: users need to be authenticated within the "user" scope by providing 34 a OAuth token (e.g. Authentication: Bearer header or `access_token` query parameter). 35 36 ```yaml 37 security: 38 - OauthSecurity: 39 - user 40 ``` 41 42 - Login and callback endpoints are not restricted: this is made explicit by overriding the default security requirement with an empty array. 43 44 ```yaml 45 paths: 46 /login: 47 get: 48 summary: login through oauth2 server 49 security: [] 50 51 ... 52 53 /auth/callback: 54 get: 55 summary: return access_token 56 security: [] 57 ``` 58 59 We need to specify a security principal in the model, to generate the server. Operations will be passed this principal as 60 parameter upon successful authentication: 61 62 ```yaml 63 definitions: 64 ... 65 principal: 66 type: string 67 ``` 68 69 In this example, the principal (descriptor of an identity for our API) 70 is just a string (i.e. the token itself). 71 72 ### Generate the server 73 74 ```shell 75 swagger generate server -A oauthSample -P models.Principal -f ./swagger.yml 76 ``` 77 78 ### Prepare the configuration 79 80 In `restapi/implementation.go` (this is not a generated file), we defined an 81 implementation for our workflow. 82 83 First, we need some extra packages to work with OAuth2, OpenID and HTTP redirections: 84 85 ```go 86 import ( 87 oidc "github.com/coreos/go-oidc" // Google OpenID client 88 "golang.org/x/net/context" 89 "golang.org/x/oauth2" // OAuth2 client 90 ) 91 ``` 92 93 ```go 94 var ( 95 // state carries an internal token during the oauth2 workflow 96 // we just need a non empty initial value 97 state = "foobar" // Don't make this a global in production. 98 99 // the credentials for this API (adapt values when registering API) 100 clientID = "" // <= enter registered API client ID here 101 clientSecret = "" // <= enter registered API client secret here 102 103 // unused in this example: the signer of the delivered token 104 issuer = "https://accounts.google.com" 105 106 // the Google login URL 107 authURL = "https://accounts.google.com/o/oauth2/v2/auth" 108 109 // the Google OAuth2 resource provider which delivers access tokens 110 tokenURL = "https://www.googleapis.com/oauth2/v4/token" 111 userInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo" 112 113 // our endpoint to be called back by the redirected client 114 callbackURL = "http://127.0.0.1:12345/api/auth/callback" 115 116 // the description of the OAuth2 flow 117 endpoint = oauth2.Endpoint{ 118 AuthURL: authURL, 119 TokenURL: tokenURL, 120 } 121 122 config = oauth2.Config{ 123 ClientID: clientID, 124 ClientSecret: clientSecret, 125 Endpoint: endpoint, 126 RedirectURL: callbackURL, 127 Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, 128 } 129 ) 130 ``` 131 132 ### Configure the API in `restapi/configure_auth_sample.go` 133 134 ```go 135 func configureAPI(api *operations.OauthSampleAPI) http.Handler { 136 // configure the api here 137 api.ServeError = errors.ServeError 138 139 // Set your custom logger if needed. Default one is log.Printf 140 // Expected interface func(string, ...interface{}) 141 // 142 // Example: 143 api.Logger = log.Printf 144 145 api.JSONConsumer = runtime.JSONConsumer() 146 147 api.JSONProducer = runtime.JSONProducer() 148 149 api.OauthSecurityAuth = func(token string, scopes []string) (*models.Principal, error) { 150 // This handler is called by the runtime whenever a route needs authentication 151 // against the 'OAuthSecurity' scheme. 152 // It is passed a token extracted from the Authentication Bearer header, and 153 // the list of scopes mentioned by the spec for this route. 154 155 // NOTE: in this simple implementation, we do not check scopes against 156 // the signed claims in the JWT token. 157 // So whatever the required scope (passed a parameter by the runtime), 158 // this will succeed provided we get a valid token. 159 160 // authenticated validates a JWT token at userInfoURL 161 ok, err := authenticated(token) 162 if err != nil { 163 return nil, errors.New(401, "error authenticate") 164 } 165 if !ok { 166 return nil, errors.New(401, "invalid token") 167 } 168 169 // returns the authenticated principal (here just filled in with its token) 170 prin := models.Principal(token) 171 return &prin, nil 172 } 173 174 api.GetAuthCallbackHandler = operations.GetAuthCallbackHandlerFunc(func(params operations.GetAuthCallbackParams) middleware.Responder { 175 // implements the callback operation 176 token, err := callback(params.HTTPRequest) 177 if err != nil { 178 return middleware.NotImplemented("operation .GetAuthCallback error") 179 } 180 log.Println("Token", token) 181 return operations.NewGetAuthCallbackDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(token)}) 182 }) 183 184 api.GetLoginHandler = operations.GetLoginHandlerFunc(func(params operations.GetLoginParams) middleware.Responder { 185 return login(params.HTTPRequest) 186 }) 187 188 api.CustomersCreateHandler = customers.CreateHandlerFunc(func(params customers.CreateParams, principal *models.Principal) middleware.Responder { 189 // other API endpoint ... 190 log.Println("hit customer API") 191 return middleware.NotImplemented("operation customers.Create has not yet been implemented") 192 }) 193 194 api.CustomersGetIDHandler = customers.GetIDHandlerFunc(func(params customers.GetIDParams, principal *models.Principal) middleware.Responder { 195 // other API endpoint ... 196 log.Println("hit customer API") 197 return middleware.NotImplemented("operation customers.GetID has not yet been implemented") 198 }) 199 200 api.ServerShutdown = func() {} 201 202 return setupGlobalMiddleware(api.Serve(setupMiddlewares)) 203 } 204 ``` 205 206 We set the following implementation for authentication in `restapi/implementation.go` (**this is not generated code** and may be fully customized): 207 208 - Redirecting to the login page 209 210 ```go 211 func login(r *http.Request) middleware.Responder { 212 // implements the login with a redirection 213 return middleware.ResponderFunc( 214 func(w http.ResponseWriter, pr runtime.Producer) { 215 http.Redirect(w, r, config.AuthCodeURL(state), http.StatusFound) 216 }) 217 } 218 ``` 219 - Retrieving the access token 220 221 ```go 222 func callback(r *http.Request) (string, error) { 223 // we expect the redirected client to call us back 224 // with 2 query params: state and code. 225 // We use directly the Request params here, since we did not 226 // bother to document these parameters in the spec. 227 228 if r.URL.Query().Get("state") != state { 229 log.Println("state did not match") 230 return "", fmt.Errorf("state did not match") 231 } 232 233 myClient := &http.Client{} 234 235 parentContext := context.Background() 236 ctx := oidc.ClientContext(parentContext, myClient) 237 238 authCode := r.URL.Query().Get("code") 239 log.Printf("Authorization code: %v\n", authCode) 240 241 // Exchange converts an authorization code into a token. 242 // Under the hood, the oauth2 client POST a request to do so 243 // at tokenURL, then redirects... 244 oauth2Token, err := config.Exchange(ctx, authCode) 245 if err != nil { 246 log.Println("failed to exchange token", err.Error()) 247 return "", fmt.Errorf("failed to exchange token") 248 } 249 250 // the authorization server's returned token 251 log.Println("Raw token data:", oauth2Token) 252 return oauth2Token.AccessToken, nil 253 } 254 ``` 255 256 257 - Validating the token 258 259 ```go 260 func authenticated(token string) (bool, error) { 261 // validates the token by sending a request at userInfoURL 262 bearToken := "Bearer " + token 263 req, err := http.NewRequest("GET", userInfoURL, nil) 264 if err != nil { 265 return false, fmt.Errorf("http request: %v", err) 266 } 267 268 req.Header.Add("Authorization", bearToken) 269 270 cli := &http.Client{} 271 resp, err := cli.Do(req) 272 if err != nil { 273 return false, fmt.Errorf("http request: %v", err) 274 } 275 defer resp.Body.Close() 276 277 _, err = ioutil.ReadAll(resp.Body) 278 if err != nil { 279 return false, fmt.Errorf("fail to get response: %v", err) 280 } 281 if resp.StatusCode != 200 { 282 return false, nil 283 } 284 return true, nil 285 } 286 ``` 287 288 ### Register the callback URL 289 Register your API at [google oauth2 server][google_credential], with 290 an OAuth ID. 291 Make sure that the callback URL is the same as set in the above code (``./restapi/configure_auth_sample.go``), e.g.: 292 293 ``` 294 http://127.0.0.1:12345/api/auth/callback 295 ``` 296 297 ![Google api screenshot](https://github.com/circl-dev/go-swagger/blob/master/examples/oauth2/img/google-api.png) 298 299 >**NOTE:** you may specify a client ID for your API during the registration process. 300 >A password (the API client's secret) is then delivered. 301 >Those are the credentials of the API itself, not the end user. 302 >Put these values (client ID and client's secret) in the initial 303 >var declarations in `implementation.go`. 304 305 ### Run the server 306 307 ```shell 308 go run ./cmd/oauth-sample-server/main.go --port 12345 309 ``` 310 311 ### Login to get the access token 312 313 Get the access token through Google's oauth2 server. 314 315 Open the browser and access the API login url on: 316 http://127.0.0.1:12345/api/login, which will direct you to the Google 317 login page. 318 319 Once you login with your google ID (e.g., your gmail account), the oauth2 320 ``access_token`` is returned and displayed on the browser. 321 322 ### Exercise your authorizer 323 324 ``TOKEN`` is obtained from the previous step. 325 326 Now we may use this token to access the other endpoints published by our API. 327 328 Let's try this with curl. Copy the received token and reuse it as shown below: 329 ```shellsession 330 ± ivan@avalon:~ 331 » curl -i -H 'Authorization: Bearer TOKEN' http://127.0.0.1:12345/api/customers 332 ``` 333 ```http 334 HTTP/1.1 501 Not Implemented 335 Content-Type: application/keyauth.api.v1+json 336 Date: Fri, 25 Nov 2016 19:14:14 GMT 337 Content-Length: 57 338 339 "operation customers.GetID has not yet been implemented" 340 ``` 341 342 Use an random string as the token: 343 344 ```shellsession 345 ± ivan@avalon:~ 346 » curl -i -H 'Authorization: Bearer RAMDOM_TOKEN' http://127.0.0.1:12345/api/customers 347 ``` 348 ```http 349 HTTP/1.1 401 Unauthorized 350 Content-Type: application/keyauth.api.v1+json 351 Date: Fri, 25 Nov 2016 19:16:49 GMT 352 Content-Length: 47 353 354 {"code":401,"message":"unauthenticated for invalid credentials"} 355 ``` 356 357 [google_credential]: https://console.cloud.google.com/apis/credentials/ 358 [example_code]: https://github.com/circl-dev/go-swagger/blob/master/examples/oauth2/