[英]用Go 实现 OAuth 2.0

653 阅读5分钟
原文链接: www.sohamkamani.com

OAuth2 is an authentication protocol that is used to authenticate and authorize users in an application by using another service provider.

This post will go through how to build a Go application to implement the OAuth2 protocol.

If you just want to see the code, you can view it here

OAuth2 flow

Let’s take a brief look at the OAuth protocol before we jump into implementation. If you’ve ever seen a dialog similar to this, then you probably have some idea of what OAuth is:

gitlab using github oauth

In the above image, we are trying to login to Gitlab using Github to authenticate.

There are three parties in any OAuth flow:

  1. The client - The person, or user who is logging in
  2. The consumer - The application that the client wants to log in to (which was gitlab in the above image)
  3. The service provider - The external application through which the user authenticates. (which was github in the above image)

In this post, we will authenticate using Githubs OAuth2 API, and build a sample Go application, running on the local port 8080, with a web interface. So in our case, the client would be the web interface, the consumer would be the application running on localhost:8080 and the service provider would be Github. Let’s look at how this would all work:

oauth flow diagram

We can implement each part of the flow in our application.

Landing page

Lets create the first part of the application, which is the landing page. This will be a simple HTML page, with a link that the user should click on to authenticate with Github. The following will go into a file public/index.html:

<!DOCTYPE html>
<html>

<body>
  <a href="https://github.com/login/oauth/authorize?client_id=myclientid123&redirect_uri=http://localhost:8080/oauth/redirect">
    Login with github
  </a>
</body>

</html>

The above link has three key parts:

  • https//github.com/login/oauth/authorize is the OAuth gateway for Github’s OAuth flow. All OAuth providers have a gateway URL that you have to send the user to in order to proceed.
  • client_id=myclientid123 - this specifies the client ID of the application. This ID will tell Github about the identity of the consumer who is trying to use their OAuth service. OAuth service providers have portal in which you can register your consumer. On registration, you will receive a client ID (which we are using here as myclientid123), and a client secret (which we will use later on). For Github, the portal to register new applications can be found on github.com/settings/ap…. After
  • redirect_uri=http://localhost:8080/oauth/redirect - specifies the URL to redirect to with the request token, once the user has been authenticated by the service provider. Normally, you will have to set this value on the registration portal as well, to prevent anyone from setting malicious callback URLs.

Next, we need to serve the index.html file we made above. The following code would go into a new file main.go:

func main() {
	fs := http.FileServer(http.Dir("public"))
	http.Handle("/", fs)

	http.ListenAndServe(":8080", nil)
}

In the current state, you can start the server (by executing go run main.go) and visit http://localhost:8080 , and you will see the landing page we just made. Once you click on the “Login with github” link, you will be redirected to the familiar OAuth page to register with Github. However, once you authenticate, you will be redirected to http://localhost:8080/oauth/redirect, which at the moment, does not do anything, and will lead to a 404 page on the server.

The redirect route

Once the user authenticates with Github, they get redirected to the redirect URL that was specified earlier. The service provider also adds a request token along with the url. In this case, Github adds this as the code parameter, so the redirect URL will actually be something like http://localhost:8080/oauth/redirect?code=mycode123, where mycode123 the request token. We need this request token, and our client secret to get the access token, which is the token that is actually used to get information about the user. We get this access token by making a POST HTTP call to https://github.com/login/oauth/access_token along with the mentioned information.

The complete documentation on the information Github provides to the redirect URL, and the information we need for provide with the POST /login/oauth/access_token HTTP call, can be found here.

Let’s add to the main.go file, to handle the /oauth/redirect route:

const clientID = "<your client id>"
const clientSecret = "<your client secret>"

func main() {
	fs := http.FileServer(http.Dir("public"))
	http.Handle("/", fs)

	// We will be using `httpClient` to make external HTTP requests later in our code
	httpClient := http.Client{}

	// Create a new redirect route route
	http.HandleFunc("/oauth/redirect", func(w http.ResponseWriter, r *http.Request) {
		// First, we need to get the value of the `code` query param
		err := r.ParseForm()
		if err != nil {
			fmt.Fprintf(os.Stdout, "could not parse query: %v", err)
			w.WriteHeader(http.StatusBadRequest)
		}
		code := r.FormValue("code")

		// Next, lets for the HTTP request to call the github oauth enpoint
		// to get our access token
		reqURL := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", clientID, clientSecret, code)
		req, err := http.NewRequest(http.MethodPost, reqURL, nil)
		if err != nil {
			fmt.Fprintf(os.Stdout, "could not create HTTP request: %v", err)
			w.WriteHeader(http.StatusBadRequest)
		}
		// We set this header since we want the response
		// as JSON
		req.Header.Set("accept", "application/json")

		// Send out the HTTP request
		res, err := httpClient.Do(req)
		if err != nil {
			fmt.Fprintf(os.Stdout, "could not send HTTP request: %v", err)
			w.WriteHeader(http.StatusInternalServerError)
		}
		defer res.Body.Close()

		// Parse the request body into the `OAuthAccessResponse` struct
		var t OAuthAccessResponse
		if err := json.NewDecoder(res.Body).Decode(&t); err != nil {
			fmt.Fprintf(os.Stdout, "could not parse JSON response: %v", err)
			w.WriteHeader(http.StatusBadRequest)
		}

		// Finally, send a response to redirect the user to the "welcome" page
		// with the access token
		w.Header().Set("Location", "/welcome.html?access_token="+t.AccessToken)
		w.WriteHeader(http.StatusFound)
	})

	http.ListenAndServe(":8080", nil)
}

type OAuthAccessResponse struct {
	AccessToken string `json:"access_token"`
}

Now, the redirect URL, if functional, and will redirect the user to the welcome page, along with the access token.

Welcome page

The welcome page is the page we show the user after they have logged in. Now that we have the users access token, we can obtain their account information on their behalf as authorized Github users.

For a list of all APIs available, you can see the Github API Documentation

We will be using the /user API to get basic info about the user and say hi to them on our welcome page. Create a new file public/welcome.html:

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>Hello</title>
</head>

<body>

</body>
<script>
	// We can get the token from the "access_token" query
	// param, available in the browsers "location" global
	const query = window.location.search.substring(1)
	const token = query.split('access_token=')[1]

	// Call the user info API using the fetch browser library
	fetch('//api.github.com/user', {
			headers: {
				// Include the token in the Authorization header
				Authorization: 'token ' + token
			}
		})
		// Parse the response as JSON
		.then(res => res.json())
		.then(res => {
			// Once we get the response (which has many fields)
			// Documented here: https://developer.github.com/v3/users/#get-the-authenticated-user
			// Write "Welcome <user name>" to the documents body
			const nameNode = document.createTextNode(`Welcome, ${res.name}`)
			document.body.appendChild(nameNode)
		})
</script>

</html>

With the addition of the welcome page, our OAuth flow is now complete! Once the app starts, one can go to http://localhost:8080/ , authorize with Github, and end up on the welcome page, which displays the greeting. My name on my github profile is “Soham Kamani”, so the weclome page will display Welcome, Soham Kamani once I login.

The source code, and instructions on how to run it can be found here

Making your app more secure

Although this post demonstrated the basics of OAuth2, there is a lot more that can be done to make your app mode secure, that isn’t covered here:

  1. In this example, we passed the access token to the client so that it can make requests as the authorized user. To make your app more secure, the access token should not be passed directly to the user. Instead, create a session token that is sent to the user as a cookie.

    The app will maintain a mapping of the session tokens to access tokens on the server side. Instead of making requests to github, the user will make requests to the server (with the session token), which will in turn use the provided session token to look up the access token and make the request to github on the server side. I have written more about sessions and cookies here.

  2. While sending the user to the authorization URL, there is a provision to provide a value for a query parameter called state. The value of this should be a random un-guessable string provided by the application. When github calls the redirect url, it will attach this state variable to the request params. The new URL would now look like: https://github.com/login/oauth/authorize?client_id=myclientid123&redirect_uri=http://localhost:8080/oauth/redirect&state=somerandomstring

    The applicaiton can now compare this value with the value it originally generated. If they are not the same, that means the request came from some third party, and should be rejected. For more information on this type on security issue, you can read my other post