- Build your Own OAuth2 Server in Go: The Client Credentials Grant Flow
- Originally written by Cyan Tarek
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: shixi – li
- Proofread by: Jake Ggie, LucaslEliane
Hi, in today’s post, I’m going to show you how to build your own OAuth2 server, just like Google, Facebook, Github, etc.
This is helpful if you want to build public or private apis for production environments. So let’s get started.
What is the OAuth2?
Open License version 2.0 is called OAuth2. It is a protocol or framework for securing RESTful Web services. OAuth2 is very powerful. Most REST apis are now secured through OAuth2 because of its rock-solid security.
OAuth2 has two parts
-
The client
-
The service side
OAuth2 client
If you’re familiar with this interface, you know what I’m talking about. But familiar or not, let me tell you the story behind this image.
You’re building a user-facing application that works with the user’s Github repository. For example: CI tools like TravisCI, CircleCI and Drone.
But users’ Github accounts are protected and no one has access to them if the owner doesn’t want to. So how do these CI tools access users’ Github accounts and repositories?
It’s actually quite simple.
Your application will ask the user
“In order to work with our service, we need access to your Github repository. Do you agree?”
And then the user will say
“I agree. You can do what you need to do.”
Your application will then request Github permission management to get Github access for that particular user. Github checks to see if this is true and asks the user to authorize it. Github then sends a temporary token to the client.
Now, when your application needs to access Github after being authenticated and authorized, you need to bring this token along in the request. Github receives it and thinks:
“Gee, this access token looks familiar. We probably gave it to you before. Ok, you can visit.”
It’s a long process. But times have changed, and now you don’t have to go to the Github licensing center every time (we never did, of course). Everything can be done automatically.
But how?
This is the UML sequence diagram that corresponds to what I discussed a few minutes ago. It’s a corresponding graphical representation.
From the picture above, we can find several important things.
OAuth2 has four roles:
-
Users – the users who end up using your application
-
Client – the application you build that uses the Github account, which is what users will use
-
Authentication server – This server handles OAuth related transactions
-
Resource server – This server has those protected resources. Such as making
The client sends an OAuth2 request to the authentication server on behalf of the user.
Building an OAuth2 client is not simple but not difficult either. Sounds like fun, right? We’ll do that in the next section.
But in this section, we’re going to look at the other side of the world. We will build our own OAuth2 server. It’s not easy but it’s fun.
Are you ready? Let’s get started
OAuth2 server
You may ask me
“Cyan wait, why build an OAuth2 server?”
Friend, have you forgotten? I told you that before. Well, let me tell you again.
Imagine that you have built a great application that provides accurate weather information (there are already many apis of this type). Now you want to make it available to the public or you want to make money off of it.
In either case, you need to protect your resources from unauthorized access or malicious attacks. So you need to protect your API resources. So that’s where OAuth2 comes in. There you are!
As you can see from the figure above, the authentication server needs to be placed before the REST API resource server. That’s what we’re talking about. This authentication server needs to be built according to the OAuth2 specification. Then we will become the Github in the first picture. Just kidding.
The primary goal of the OAuth2 server is to provide the client with a token to access. This is why OAuth2 servers are also called OAuth2 providers because they can provide tokens.
So much for the explanation.
There are four different OAuth2 server modes based on the authentication process:
-
Authorization code mode
-
Implicit authorization mode
-
Client authentication mode
-
Password mode
If you want to learn more about OAuth2, check out the great article here.
In this article, we will use client-side authentication mode. Let’s take a closer look.
Server-based client credential authorization process
There are a few things we need to know when building the client-side credential authorization process based on the OAuth2 server.
There is no user interaction (i.e. no registration, login) in this authorization type. Instead, you need two things, which are the client ID and the client key. With these two things, we can get access tokens. A client is a third-party application. This is convenient and appropriate when you need to access a resource server without a user mechanism or only through a client application.
This is the corresponding UML sequence diagram.
coding
To build this project, we need to rely on a great Go language package.
First, we need to develop a simple API service as a resource server.
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}, srv))
log.Fatal(http.ListenAndServe(": 9096", nil))
}
Copy the code
Run the service and to send a Get request to http://localhost:9096/protected
You’ll get a response.
What kind of protection does this service have?
Even if the name of the interface is protected, anyone can request it. We need to protect this interface with OAuth2.
Now we need to write our own authorization service.
routing
-
/ Credentials are used to issue client credentials (client ids and client keys)
-
/token Issues tokens using client credentials
We need to implement both routes.
Here is the preliminary setup
package main
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
"gopkg.in/oauth2.v3/models"
"log"
"net/http"
"time"
"gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store"
)
func main() {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
manager.MustTokenStorage(store.NewMemoryTokenStore())
clientStore := store.NewClientStore()
manager.MapClientStorage(clientStore)
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(true)
srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
http.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
})
log.Fatal(http.ListenAndServe(": 9096", nil))
}
Copy the code
Here we create a manager for client storage and the authentication service itself.
Here is an implementation of the /credentials route:
http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
clientId := uuid.New().String()[:8]
clientSecret := uuid.New().String()[:8]
err := clientStore.Set(clientId, &models.Client{
ID: clientId,
Secret: clientSecret,
Domain: "http://localhost:9094",})iferr ! = nil { fmt.Println(err.Error()) } w.Header().Set("Content-Type"."application/json")
json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
})
Copy the code
It creates two random strings, one for the client ID and the other for the client key. And save them to client storage. And then you get a response back. That’s it. We are using in-memory storage here, but we can also store them in Redis, mongodb, Postgres, etc.
Here is an implementation of the /token route:
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
srv.HandleTokenRequest(w, r)
})
Copy the code
It’s very simple. It passes the request and response to the appropriate handler so that the server can decode all the necessary data in the request.
So here’s our overall code:
package main
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
"gopkg.in/oauth2.v3/models"
"log"
"net/http"
"time"
"gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store"
)
func main() {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
manager.MustTokenStorage(store.NewMemoryTokenStore())
clientStore := store.NewClientStore()
manager.MapClientStorage(clientStore)
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(true)
srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
srv.HandleTokenRequest(w, r)
})
http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
clientId := uuid.New().String()[:8]
clientSecret := uuid.New().String()[:8]
err := clientStore.Set(clientId, &models.Client{
ID: clientId,
Secret: clientSecret,
Domain: "http://localhost:9094",})iferr ! = nil { fmt.Println(err.Error()) } w.Header().Set("Content-Type"."application/json")
json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
})
http.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
})
log.Fatal(http.ListenAndServe(": 9096", nil))
}
Copy the code
Run this code and to http://localhost:9096/credentials routing to register and obtain the client ID and a client secret key.
Now go to this link http://localhost:9096/token? grant_type=client_credentials&client_id=2e14f7dd&client_secret=c729e9d0&scope=all
You can get an authorization token with an expiration date and some other information.
Now we have our authorization token. But our /protected route is still not protected. We need to set up a method to check that each client request has a valid token. If so, we can authorize the client. Otherwise, authorization cannot be granted.
We can do this with a piece of middleware.
Writing middleware in Golang can be fun if you know what you’re doing. Here is the code for the middleware:
func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := srv.ValidationBearerToken(r)
iferr ! = nil { http.Error(w, err.Error(), http.StatusBadRequest)return
}
f.ServeHTTP(w, r)
})
}
Copy the code
This checks whether the request has a valid token and takes action.
Now we need to use the adapter/decorator pattern to place the middleware in front of our /protected route.
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}, srv))
Copy the code
The entire code now looks like this:
package main
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
"gopkg.in/oauth2.v3/models"
"log"
"net/http"
"time"
"gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store"
)
func main() {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
// token memory store
manager.MustTokenStorage(store.NewMemoryTokenStore())
// client memory store
clientStore := store.NewClientStore()
manager.MapClientStorage(clientStore)
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(true)
srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
srv.HandleTokenRequest(w, r)
})
http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
clientId := uuid.New().String()[:8]
clientSecret := uuid.New().String()[:8]
err := clientStore.Set(clientId, &models.Client{
ID: clientId,
Secret: clientSecret,
Domain: "http://localhost:9094",})iferr ! = nil { fmt.Println(err.Error()) } w.Header().Set("Content-Type"."application/json")
json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
})
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}, srv))
log.Fatal(http.ListenAndServe(": 9096", nil))
}
func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := srv.ValidationBearerToken(r)
iferr ! = nil { http.Error(w, err.Error(), http.StatusBadRequest)return
}
f.ServeHTTP(w, r)
})
}
Copy the code
Now run the service and access the /protected interface at the URL without the access token. Or try to use the wrong access token. The authentication service will block you either way.
Now get the authentication information and access token from the server again and send the request to the protected interface:
http://localhost:9096/test?access_token=YOUR_ACCESS_TOKEN
Yes! You have access now.
Now we’ve learned how to use Go to set up our own OAuth2 server.
In the next section. We will build our own OAuth2 client in Go. And in the last part, we’ll build our own server-based authorization code pattern based on login and authorization.
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.