github.com/rzurga/go-swagger@v0.28.1-0.20211109195225-5d1f453ffa3a/docs/tutorial/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 // implements the login operation 186 login(params.HTTPRequest) 187 return middleware.NotImplemented("operation .GetLogin has not yet been implemented") 188 }) 189 190 api.CustomersCreateHandler = customers.CreateHandlerFunc(func(params customers.CreateParams, principal *models.Principal) middleware.Responder { 191 // other API endpoint ... 192 log.Println("hit customer API") 193 return middleware.NotImplemented("operation customers.Create has not yet been implemented") 194 }) 195 196 api.CustomersGetIDHandler = customers.GetIDHandlerFunc(func(params customers.GetIDParams, principal *models.Principal) middleware.Responder { 197 // other API endpoint ... 198 log.Println("hit customer API") 199 return middleware.NotImplemented("operation customers.GetID has not yet been implemented") 200 }) 201 202 api.ServerShutdown = func() {} 203 204 return setupGlobalMiddleware(api.Serve(setupMiddlewares)) 205 } 206 ``` 207 208 We set the following implementation for authentication in `restapi/implementation.go` (**this is not generated code** and may be fully customized): 209 210 - Redirecting to the login page 211 212 ```go 213 func login(r *http.Request) string { 214 // implements the login with a redirection and an access token 215 var accessToken string 216 wG := r.Context().Value(ctxResponseWriter).(http.ResponseWriter) 217 http.Redirect(wG, r, config.AuthCodeURL(state), http.StatusFound) 218 return accessToken 219 } 220 ``` 221 - Retrieving the access token 222 223 ```go 224 func callback(r *http.Request) (string, error) { 225 // we expect the redirected client to call us back 226 // with 2 query params: state and code. 227 // We use directly the Request params here, since we did not 228 // bother to document these parameters in the spec. 229 230 if r.URL.Query().Get("state") != state { 231 log.Println("state did not match") 232 return "", fmt.Errorf("state did not match") 233 } 234 235 myClient := &http.Client{} 236 237 parentContext := context.Background() 238 ctx := oidc.ClientContext(parentContext, myClient) 239 240 authCode := r.URL.Query().Get("code") 241 log.Printf("Authorization code: %v\n", authCode) 242 243 // Exchange converts an authorization code into a token. 244 // Under the hood, the oauth2 client POST a request to do so 245 // at tokenURL, then redirects... 246 oauth2Token, err := config.Exchange(ctx, authCode) 247 if err != nil { 248 log.Println("failed to exchange token", err.Error()) 249 return "", fmt.Errorf("failed to exchange token") 250 } 251 252 // the authorization server's returned token 253 log.Println("Raw token data:", oauth2Token) 254 return oauth2Token.AccessToken, nil 255 } 256 ``` 257 258 259 - Validating the token 260 261 ```go 262 func authenticated(token string) (bool, error) { 263 // validates the token by sending a request at userInfoURL 264 bearToken := "Bearer " + token 265 req, err := http.NewRequest("GET", userInfoURL, nil) 266 if err != nil { 267 return false, fmt.Errorf("http request: %v", err) 268 } 269 270 req.Header.Add("Authorization", bearToken) 271 272 cli := &http.Client{} 273 resp, err := cli.Do(req) 274 if err != nil { 275 return false, fmt.Errorf("http request: %v", err) 276 } 277 defer resp.Body.Close() 278 279 _, err = ioutil.ReadAll(resp.Body) 280 if err != nil { 281 return false, fmt.Errorf("fail to get response: %v", err) 282 } 283 if resp.StatusCode != 200 { 284 return false, nil 285 } 286 return true, nil 287 } 288 ``` 289 290 ### Register the callback URL 291 Register your API at [google oauth2 server][google_credential], with 292 an OAuth ID. 293 Make sure that the callback URL is the same as set in the above code (``./restapi/configure_auth_sample.go``), e.g.: 294 295 ``` 296 http://127.0.0.1:12345/api/auth/callback 297 ``` 298 299  300 301 >**NOTE:** you may specify a client ID for your API during the registration process. 302 >A password (the API client's secret) is then delivered. 303 >Those are the credentials of the API itself, not the end user. 304 >Put these values (client ID and client's secret) in the initial 305 >var declarations in `implementation.go`. 306 307 ### Run the server 308 309 ```shell 310 go run ./cmd/oauth-sample-server/main.go --port 12345 311 ``` 312 313 ### Login to get the access token 314 315 Get the access token through Google's oauth2 server. 316 317 Open the browser and access the API login url on: 318 http://127.0.0.1:12345/api/login, which will direct you to the Google 319 login page. 320 321 Once you login with your google ID (e.g., your gmail account), the oauth2 322 ``access_token`` is returned and displayed on the browser. 323 324 ### Exercise your authorizer 325 326 ``TOKEN`` is obtained from the previous step. 327 328 Now we may use this token to access the other endpoints published by our API. 329 330 Let's try this with curl. Copy the received token and reuse it as shown below: 331 ```shellsession 332 ± ivan@avalon:~ 333 » curl -i -H 'Authorization: Bearer TOKEN' http://127.0.0.1:12345/api/customers 334 ``` 335 ```http 336 HTTP/1.1 501 Not Implemented 337 Content-Type: application/keyauth.api.v1+json 338 Date: Fri, 25 Nov 2016 19:14:14 GMT 339 Content-Length: 57 340 341 "operation customers.GetID has not yet been implemented" 342 ``` 343 344 Use an random string as the token: 345 346 ```shellsession 347 ± ivan@avalon:~ 348 » curl -i -H 'Authorization: Bearer RAMDOM_TOKEN' http://127.0.0.1:12345/api/customers 349 ``` 350 ```http 351 HTTP/1.1 401 Unauthorized 352 Content-Type: application/keyauth.api.v1+json 353 Date: Fri, 25 Nov 2016 19:16:49 GMT 354 Content-Length: 47 355 356 {"code":401,"message":"unauthenticated for invalid credentials"} 357 ``` 358 359 [google_credential]: https://console.cloud.google.com/apis/credentials/ 360 [example_code]: https://github.com/go-swagger/go-swagger/blob/master/examples/oauth2/