OAuth or open authorization is a standard protocol to authorize a 3rd party client to access resources on behalf of a user.
Let’s take an example to understand this statement. We all have a Gmail address. But Gmail doesn’t have a desktop client. Therefore, you need to use a 3rd party desktop mail client e.g., Thunderbird or Mailspring. But the problem is, how a 3rd party email client can access your Gmail inbox?
To solve this problem, email clients use OAuth. When you log in using OAuth, you give authorization to the email client to access your Gmail on behalf of you.
You want another example to get a good understanding? Ok let’s take an example of automated website deployment in Netlify. You can link your GitHub or Gitlab repository and deploy your website very easily on Netlify.
To perform a deployment, first these platforms should pull your repository from GitHub. But they can’t do that unless you authorize them to do so. You can give them this authorization using GitHub OAuth and make a successful deployment.
Why developer use OAuth to authenticate a user
Google, GitHub, GitLab etc are called OAuth providers. There are numerous OAuth providers present, but these are the most popular among developer community.
When a user creates an account on their website, they collect basic user information like name, email, profile picture etc. OAuth providers provide The user information from an API endpoint.
Now as a developer, you build applications. You want basic user information to authenticate and create user accounts. OAuth gives you certain advantages over traditional email-password authentication.
- You don’t need to store passwords.
- User email, name, and images are readily available. Making account creation process smooth and easy.
- Don’t need to filter spam and bots. OAuth provider takes care of it.
After all this explanation, You may understand the reason to use OAuth to authenticate a user. Now let’s understand the process by which OAuth works.
Understand the OAuth flow
Let’s again take an example to understand the OAuth flow. Here I am focusing on server-side OAuth implementation.
Every OAuth provider issue a client ID and client secret to use their OAuth service. Make sure to get a pair of these credentials.
Assume you have 3 routes defined in your server, /login
, /callback
and /dashboard
.
/login
: This route takes a login request and generates a unique OAuth URL using client ID and Client secret. Then redirect the user to the generated URL. The generated URL lands on the OAuth provider website./callback
: Once client completes the authentication process on the OAuth provider website, he/she redirect back to the original website to a predefined route. This route process the redirected callback from the OAuth provider./dashboard
: This is a user dashboard. A protected route. When the/callback
route successfully create a user account, user is redirected to this route.
We can divide the OAuth flow into 9 steps.
- User comes to the
/login
route. - In the
/login
route, server generate a unique OAuth URL and redirect the user to that URL. - User Follow this unique URL and land on the OAuth provider website.
- User give their credentials and login on the provider’s website.
- On successful login, user redirected to the
/callback
URL. - The redirect URL contain a
code
in the query parameter. - Inside the
/callback
route, thecode
parameter is extracted from the query parameter. - Server exchange this
code
to get anAccess Token
from the OAuth provider. - Server call OAuth provider API with the
Access Token
to get the user information. - Server use this information to create a user account.
There are some additional information you should keep in your mind.
- There are 2 access types in the OAuth protocol. If the OAuth access type is set to offline, a
Refresh Token
is generated in addition to anAccess Token
. ThisRefresh Token
can be used to regenerate theAccess Token
without re-authenticating the user. - While generating the unique OAuth URL in the
/login
route, astate
variable can be passed in the request URL. The OAuth provider return the samestate
variable in/callback
route. You can use this feature to restore the client state or mitigate cross site request forgery (CSRF) attack.
OAuth2 in Golang
Golang has an official OAuth2 implementation in golang.org/x/oauth2
package. There are also multiple OAuth2 provider specific packages like golang.org/x/oauth2/google
, golang.org/x/oauth2/facebook
, golang.org/x/oauth2/github
etc.
A complete example for this blog is present in my GitHub profile. Do check this out.
Creating the OAuth2 configuration
To use this OAuth2 package, first you need to create an oAuth2.config
.
// Code copied from : https://pkg.go.dev/golang.org/x/oauth2#Config
type Config struct {
// ClientID is the application's ID.
ClientID [string] //(https://pkg.go.dev/builtin#string)
// ClientSecret is the application's secret.
ClientSecret [string] //(https://pkg.go.dev/builtin#string)
// Endpoint contains the resource server's token endpoint
// URLs. These are constants specific to each server and are
// often available via site-specific packages, such as
// google.Endpoint or github.Endpoint.
Endpoint [Endpoint] //(https://pkg.go.dev/golang.org/x/oauth2#Endpoint)
// RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs.
RedirectURL [string] //(https://pkg.go.dev/builtin#string)
// Scope specifies optional requested permissions.
Scopes [][string] //(https://pkg.go.dev/builtin#string)
// contains filtered or unexported fields
}
This is the definition of OAUth2 config. Let’s understand the parameters in the Config
struct.
You can get the ClientID
and ClientSecret
from the OAuth provider.
OAuth provider specific packages contain the Endpoint
. For example, to use Google endpoint, use golang.org/x/oauth2/google
package.
When a user completes their authentication, they are redirected to the RedirectedURL
. Make sure to register this URL in the OAuth provider website.
Scope
contain the scope of data you want to access from the OAuth provider on behalf of the user. In most cases, you just need the user email and basic information. For that, use ["email","user"]
as scope.
This is an example of the OAuth2 config.
// access them from environment variable
clientid := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SEC")
// Oauth config manage the OAuth flow. You have to register
// the redirect url in the OAuth provider. For the endpoint,
// there are many provider specific package inside the
// golang.org/x/oauth2 package
conf := &oauth2.Config{
ClientID: clientid,
ClientSecret: clientSecret,
RedirectURL: "http://localhost:8000/auth/callback",
Scopes: []string{"email", "profile"},
Endpoint: google.Endpoint,
}
Generating OAUth2 URL
In the previous step you have created an instance of oauth2.config
. We can use the AuthCodeURL
method to generate an OAuth URL.
url := conf.AuthCodeURL("some-user-state", oauth2.AccessTypeOffline)
The AuthCodeURL
method accepts 2 parameters. First string
parameter is called state
. You can use this state to restore the user session. State is also used to mitigate CSRF attack.
The second parameter is used to define the access type. This access type may be online of offline.
-
Online access type: on successful authentication, it gives an access token. You can use this access token to retrieve user information from the OAuth provider.
-
Offline access type: On successful authentication, it gives a refresh token in addition to an access token. Refresh token can be used to regenerate the access token.
Then you redirect the user to this generated OAuth2 URL.
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
Process OAuth2 response code
When a user completed the authentication process on the OAuth2 provider’s website, it redirects the user to the redirect url
. You have to provide the redirect url
beforehand, during Oauth2 client registration.
The redirect URL contain 2 query parameter, code
and state
.
code
: This query parameter contains the access code. Access code is required to get the access token.state
: The OAuth provider echoes the state value set during the OAuth URL generation step. You can usestate
parameter to verify previously setstate
and restore the user state.
Retrieving the access token from code
is a 2-step process.
- Get the
code
from query parameter.
code := r.URL.Query().Get("code")
- Exchange the
code
for anaccess token
. TheExchange
method communicate with the Oauth provider and fetch theaccess token
.
t, err := conf.Exchange(context.Background(), code)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Here, t
is a struct
that contain the access token and refresh token.
Retrieve user information
The access token is used to communicate with the Oauth provider, on behalf of the user. Follow these steps to retrive user email and public information.
- Make an HTTP client from the Oauth config. If your access token expires, this method automatically generates the
access token
fromrefresh token
.
client := conf.Client(context.Background(), t)
- Make a GET request to the user endpoint to retrieve user information. The URL is different for different provider.
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- Read the response body. The response is a JSON encoded value.
defer resp.Body.Close()
var v any
// Reading the JSON body using JSON decoder
err = json.NewDecoder(resp.Body).Decode(&v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// printing the json data into std out.
fmt.printf("%v", v)
Google login in Golang
The golang.org/x/oauth2/google
package contain the Google oauth related configurations and methods.
Please read the OAuth in Golang section before following these steps.
- Install the Google oauth2 package.
go get golang.org/x/oauth2/google
- Get the client ID and client secret from the Google cloud credentials dashboard.
- Add
google.endpoint
in the OAuth2 configuration.
conf := &oauth2.Config{
ClientID: clientid,
ClientSecret: clientSecret,
RedirectURL: "http://localhost:8000/auth/callback",
Scopes: []string{"email", "profile"},
Endpoint: google.Endpoint,
}
- Make a
/auth/login/google
route to generate Google OAuth URL and redirect the user to the Google login page.
mux.HandleFunc("GET /auth/login/google", googleHandler)
func googleHandler(w http.ResponseWriter, r *http.Request) {
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
- Make a
/auth/callback/google
route to handle callback from Google OAuth and get thecode
.
mux.HandleFunc("GET /auth/callback/google", googleCallbackHandler)
func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
}
- In the same handler, exchange this
code
for anaccess token
.
func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
t, err := conf.Exchange(context.Background(), code)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
- Use this
access token
to make an HTTP client. This method auto-generate theaccess token
if it is expired.
func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
t, err := conf.Exchange(context.Background(), code)
...
client := conf.Client(context.Background(), t)
}
- Use the HTTP client to fetch user information from Google.
func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
t, err := conf.Exchange(context.Background(), code)
...
client := conf.Client(context.Background(), t)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
- Read the response using a JSON decoder.
func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
t, err := conf.Exchange(context.Background(), code)
...
client := conf.Client(context.Background(), t)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
...
defer resp.Body.Close()
var v any
// Reading the JSON body using JSON decoder
err = json.NewDecoder(resp.Body).Decode(&v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
- Persist the user data and redirect the user to a protected page.
func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
t, err := conf.Exchange(context.Background(), code)
...
client := conf.Client(context.Background(), t)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
...
defer resp.Body.Close()
var v any
// Reading the JSON body using JSON decoder
err = json.NewDecoder(resp.Body).Decode(&v)
...
http.Redirect(w, r, "/dashboard", http.StatusTemporaryRedirect)
}
GitHub login in Golang
GitHub OAuth login is very similar to Google login. But you have to make some changes. I am listing the changes in the following steps.
- Get the GitHub OAuth2 package
go get golang.org/x/oauth2/github
- Set Oauth2 configuration endpoint to GitHub endpoint.
conf := &oauth2.Config{
...
Endpoint: github.Endpoint,
}
- Change the API URL to get user details from GitHub.
resp, err := client.Get("https://api.github.com/user")
Manage User session after authentication
After a user is successfully authenticated by a OAuth provider, a user session should be maintained. There are many options to manage a user session, e.g, Gorilla session, SCS etc.
I personally like SCS due to its simplicity. Let’s see how to use the scs
package to manage user sessions.
- Get the
scs
package.
go get github.com/alexedwards/scs/v2
- Initialize the session manager. By default, session is valid for 24 hours.
var sessionManager *scs.SessionManager
func main(){
...
sessionManager = scs.New()
...
}
- Add SCS middleware to handle all the heavy-lifting for you.
http.ListenAndServe(":4000", sessionManager.LoadAndSave(mux))
- Inside the
googleCallbackHandler
, set the email of the user as session data.
func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
...
err = json.NewDecoder(resp.Body).Decode(&v)
sessionManager.Put(r.Context(), "email", v.Email)
...
}
- To read the session data inside a handler, use the
Get
method.
email := sessionManager.GetString(r.Context(), "email")
Conclusion
In this blog, you have learned how OAuth flow actually works. Then I have explained the OAuth2 library in Golang. The library is implimented very nicely and very easy to use. Finally, I gave you example for Google OAuth login and GitHub OAuth Login in Golang code examples. In addition, you have also learned the use of a session manager to manage the auth state of the user.
Hope you like this blog. Subscribe for my newsletter to get notification about my latest blogs and some of my very intersting findings.