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/