- Go Programming Blueprints(Second Edition)
- Mat Ryer
- 2634字
- 2021-07-08 10:40:03
Implementing external logging in
In order to make use of the projects, clients, or accounts that we created on the authorization provider sites, we have to tell gomniauth
which providers we want to use and how we will interact with them. We do this by calling the WithProviders
function on the primary gomniauth
package. Add the following code snippet to main.go
(just underneath the flag.Parse()
line toward the top of the main
function):
// setup gomniauth gomniauth.SetSecurityKey("PUT YOUR AUTH KEY HERE") gomniauth.WithProviders( facebook.New("key", "secret", "http://localhost:8080/auth/callback/facebook"), github.New("key", "secret", "http://localhost:8080/auth/callback/github"), google.New("key", "secret", "http://localhost:8080/auth/callback/google"), )
You should replace the key
and secret
placeholders with the actual values you noted down earlier. The third argument represents the callback URL that should match the ones you provided when creating your clients on the provider's website. Notice the second path segment is callback
; while we haven't implemented this yet, this is where we handle the response from the authorization process.
As usual, you will need to ensure all the appropriate packages are imported:
import ( "github.com/stretchr/gomniauth/providers/facebook" "github.com/stretchr/gomniauth/providers/github" "github.com/stretchr/gomniauth/providers/google" )
Note
Gomniauth requires the SetSecurityKey
call because it sends state data between the client and server along with a signature checksum, which ensures that the state values are not tempered with while being transmitted. The security key is used when creating the hash in a way that it is almost impossible to recreate the same hash without knowing the exact security key. You should replace some long key
with a security hash or phrase of your choice.
Logging in
Now that we have configured Gomniauth, we need to redirect users to the provider's authorization page when they land on our /auth/login/{provider}
path. We just have to update our loginHandler
function in auth.go
:
func loginHandler(w http.ResponseWriter, r *http.Request) { segs := strings.Split(r.URL.Path, "/") action := segs[2] provider := segs[3] switch action { case "login": provider, err := gomniauth.Provider(provider) if err != nil { http.Error(w, fmt.Sprintf("Error when trying to get provider %s: %s",provider, err), http.StatusBadRequest) return } loginUrl, err := provider.GetBeginAuthURL(nil, nil) if err != nil { http.Error(w, fmt.Sprintf("Error when trying to GetBeginAuthURL for %s:%s", provider, err), http. StatusInternalServerError) return } w.Header.Set("Location", loginUrl) w.WriteHeader(http.StatusTemporaryRedirect) default: w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "Auth action %s not supported", action) } }
We do two main things here. First, we use the gomniauth.Provider
function to get the provider object that matches the object specified in the URL (such as google
or github
). Then, we use the GetBeginAuthURL
method to get the location where we must send users to in order to start the authorization process.
Note
The GetBeginAuthURL(nil, nil)
arguments are for the state and options respectively, which we are not going to use for our chat application.
The first argument is a state map of data that is encoded and signed and sent to the authentication provider. The provider doesn't do anything with the state; it just sends it back to our callback endpoint. This is useful if, for example, we want to redirect the user back to the original page they were trying to access before the authentication process intervened. For our purpose, we have only the /chat
endpoint, so we don't need to worry about sending any state.
The second argument is a map of additional options that will be sent to the authentication provider, which somehow modifies the behavior of the authentication process. For example, you can specify your own scope
parameter, which allows you to make a request for permission to access additional information from the provider. For more information about the available options, search for OAuth2 on the Internet or read the documentation for each provider, as these values differ from service to service.
If our code gets no error from the GetBeginAuthURL
call, we simply redirect the user's browser to the returned URL.
If errors occur, we use the http.Error
function to write the error message out with a non-200
status code.
Rebuild and run the chat application:
go build -o chat ./chat -host=":8080"
Tip
We will continue to stop, rebuild, and run our projects manually throughout this book, but there are some tools that will take care of this for you by watching for changes and restarting Go applications automatically. If you're interested in such tools, check out https://github.com/pilu/fresh and https://github.com/codegangsta/gin.
Open the main chat page by accessing http://localhost:8080/chat
. As we aren't logged in yet, we are redirected to our sign-in page. Click on the Google option to sign in using your Google account and you will notice that you are presented with a Google-specific sign-in page (if you are not already signed in to Google). Once you are signed in, you will be presented with a page asking you to give permission for our chat application before you can view basic information about your account:

This is the same flow that the users of our chat application will experience when signing in.
Click on Accept and you will notice that you are redirected to our application code but presented with an Auth action callback not supported
error. This is because we haven't yet implemented the callback functionality in loginHandler
.
Handling the response from the provider
Once the user clicks on Accept on the provider's website (or if they click on the equivalent of Cancel), they will be redirected to the callback endpoint in our application.
A quick glance at the complete URL that comes back shows us the grant code that the provider has given us:
http://localhost:8080/auth/callback/google?code=4/Q92xJ- BQfoX6PHhzkjhgtyfLc0Ylm.QqV4u9AbA9sYguyfbjFEsNoJKMOjQI
We don't have to worry about what to do with this code because Gomniauth does it for us; we can simply jump to implementing our callback handler. However, it's worth knowing that this code will be exchanged by the authentication provider for a token that allows us to access private user data. For added security, this additional step happens behind the scenes, from server to server rather than in the browser.
In auth.go
, we are ready to add another switch case to our action path segment. Insert the following code before the default case:
case "callback": provider, err := gomniauth.Provider(provider) if err != nil { http.Error(w, fmt.Sprintf("Error when trying to get provider %s: %s", provider, err), http.StatusBadRequest) return } creds, err := provider.CompleteAuth(objx.MustFromURLQuery(r.URL.RawQuery)) if err != nil { http.Error(w, fmt.Sprintf("Error when trying to complete auth for %s: %s", provider, err), http.StatusInternalServerError) return } user, err := provider.GetUser(creds) if err != nil { http.Error(w, fmt.Sprintf("Error when trying to get user from %s: %s", provider, err), http.StatusInternalServerError) return } authCookieValue := objx.New(map[string]interface{}{ "name": user.Name(), }).MustBase64() http.SetCookie(w, &http.Cookie{ Name: "auth", Value: authCookieValue, Path: "/"}) w.Header().Set("Location", "/chat") w.WriteHeader(http.StatusTemporaryRedirect)
When the authentication provider redirects the users after they have granted permission, the URL specifies that it is a callback action. We look up the authentication provider as we did before and call its CompleteAuth
method. We parse RawQuery
from the request into objx.Map
(the multipurpose map type that Gomniauth uses), and the CompleteAuth
method uses the values to complete the OAuth2 provider handshake with the provider. All being well, we will be given some authorized credentials with which we will be able to access our user's basic data. We then use the GetUser
method for the provider, and Gomniauth will use the specified credentials to access some basic information about the user.
Once we have the user data, we Base64-encode the Name
field in a JSON object and store it as a value for our auth
cookie for later use.
Tip
Base64-encoding data ensures it won't contain any special or unpredictable characters, which is useful for situations such as passing data to a URL or storing it in a cookie. Remember that although Base64-encoded data looks encrypted, it is not you can easily decode Base64-encoded data back to the original text with little effort. There are online tools that do this for you.
After setting the cookie, we redirect the user to the chat page, which we can safely assume was the original destination.
Once you build and run the code again and hit the /chat
page, you will notice that the sign up flow works and we are finally allowed back to the chat page. Most browsers have an inspector or a console—a tool that allows you to view the cookies that the server has sent you-that you can use to see whether the auth
cookie has appeared:
go build -o chat ./chat -host=":8080"
In our case, the cookie value is eyJuYW1lIjoiTWF0IFJ5ZXIifQ==
, which is a Base64-encoded version of {"name":"Mat Ryer"}
. Remember, we never typed in a name in our chat application; instead, Gomniauth asked Google for a name when we opted to sign in with Google. Storing non-signed cookies like this is fine for incidental information, such as a user's name; however, you should avoid storing any sensitive information using non-signed cookies as it's easy for people to access and change the data.
Presenting the user data
Having the user data inside a cookie is a good start, but non-technical people will never even know it's there, so we must bring the data to the fore. We will do this by enhancing templateHandler
that first passes the user data to the template's Execute
method; this allows us to use template annotations in our HTML to display the user data to the users.
Update the ServeHTTP
method of templateHandler
in main.go
:
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t.once.Do(func() { t.templ = template.Must(template.ParseFiles(filepath.Join("templates", t.filename))) }) data := map[string]interface{}{ "Host": r.Host, } if authCookie, err := r.Cookie("auth"); err == nil { data["UserData"] = objx.MustFromBase64(authCookie.Value) } t.templ.Execute(w, data) }
Instead of just passing the entire http.Request
object to our template as data, we are creating a new map[string]interface{}
definition for a data object that potentially has two fields: Host
and UserData
(the latter will only appear if an auth
cookie is present). By specifying the map type followed by curly braces, we are able to add the Host
entry at the same time as making our map while avoiding the make
keyword altogether. We then pass this new data
object as the second argument to the Execute
method on our template.
Now we add an HTML file to our template source to display the name. Update the chatbox
form in chat.html
:
<form id="chatbox"> {{.UserData.name}}:<br/> <textarea></textarea> <input type="submit" value="Send" /> </form>
The {{.UserData.name}}
annotation tells the template engine to insert our user's name before the textarea
control.
Tip
Since we're using the objx
package, don't forget to run go get http://github.com/stretchr/objx
and import it. Additional dependencies add complexity to projects, so you may decide to copy and paste the appropriate functions from the package or even write your own code that marshals between Base64-encoded cookies and back.
Alternatively, you can vendor the dependency by copying the whole source code to your project (inside a root-level folder called vendor
). Go will, at build time, first check the vendor folder for any imported packages before checking them in $GOPATH
(which were put there by go get
). This allows you to fix the exact version of a dependency rather than rely on the fact that the source package hasn't changed since you wrote your code.
For more information about using vendors in Go, check out Daniel Theophanes' post on the subject at https://blog.gopheracademy.com/advent-2015/vendor-folder/ or search for vendoring in Go
.
Rebuild and run the chat application again and you will notice the addition of your name before the chat box:
go build -o chat ./chat -host=":8080"
Augmenting messages with additional data
So far, our chat application has only transmitted messages as slices of bytes or []byte
types between the client and the server; therefore, the forward channel for our room has the chan []byte
type. In order to send data (such as who sent it and when) in addition to the message itself, we enhance our forward channel and also how we interact with the web socket on both ends.
Define a new type that will replace the []byte
slice by creating a new file called message.go
in the chat
folder:
package main import ( "time" ) // message represents a single message type message struct { Name string Message string When time.Time }
The message
type will encapsulate the message string itself, but we have also added the Name
and When
fields that respectively hold the user's name and a timestamp of when the message was sent.
Since the client
type is responsible for communicating with the browser, it needs to transmit and receive more than just a single message string. As we are talking to a JavaScript application (that is, the chat client running in the browser) and the Go standard library has a great JSON implementation, this seems like the perfect choice to encode additional information in the messages. We will change the read
and write
methods in client.go
to use the ReadJSON
and WriteJSON
methods on the socket, and we will encode and decode our new message
type:
func (c *client) read() { defer c.socket.Close() for { var msg *message err := c.socket.ReadJSON(&msg) if err != nil { return } msg.When = time.Now() msg.Name = c.userData["name"].(string) c.room.forward <- msg } } func (c *client) write() { defer c.socket.Close() for msg := range c.send { err := c.socket.WriteJSON(msg) if err != nil { break } } }
When we receive a message from the browser, we will expect to populate only the Message
field, which is why we set the When
and Name
fields ourselves in the preceding code.
You will notice that when you try to build the preceding code, it complains about a few things. The main reason is that we are trying to send a *message
object down our forward
and send chan []byte
channels. This is not allowed until we change the type of the channel. In room.go
, change the forward
field to be of the type chan *message
, and do the same for the send chan
type in client.go
.
We must update the code that initializes our channels since the types have now changed. Alternatively, you can wait for the compiler to raise these issues and fix them as you go. In room.go
, you need to make the following changes:
- Change
forward: make(chan []byte)
toforward: make(chan *message)
- Change
r.tracer.Trace("Message received: ", string(msg))
tor.tracer.Trace("Message received: ", msg.Message)
- Change
send: make(chan []byte, messageBufferSize)
tosend: make(chan *message, messageBufferSize)
The compiler will also complain about the lack of user data on the client, which is a fair point because the client
type has no idea about the new user data we have added to the cookie. Update the client
struct to include a new general-purpose map[string]interface{}
called userData
:
// client represents a single chatting user. type client struct { // socket is the web socket for this client. socket *websocket.Conn // send is a channel on which messages are sent. send chan *message // room is the room this client is chatting in. room *room // userData holds information about the user userData map[string]interface{} }
The user data comes from the client cookie that we access through the http.Request
object's Cookie
method. In room.go
, update ServeHTTP
with the following changes:
func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) { socket, err := upgrader.Upgrade(w, req, nil) if err != nil { log.Fatal("ServeHTTP:", err) return } authCookie, err := req.Cookie("auth") if err != nil { log.Fatal("Failed to get auth cookie:", err) return } client := &client{ socket: socket, send: make(chan *message, messageBufferSize), room: r, userData: objx.MustFromBase64(authCookie.Value), } r.join <- client defer func() { r.leave <- client }() go client.write() client.read() }
We use the Cookie
method on the http.Request
type to get our user data before passing it to the client. We are using the objx.MustFromBase64
method to convert our encoded cookie value back into a usable map object.
Now that we have changed the type being sent and received on the socket from []byte
to *message
, we must tell our JavaScript client that we are sending JSON instead of just a plain string. Also, we must ask that it send JSON back to the server when a user submits a message. In chat.html
, first update the socket.send
call:
socket.send(JSON.stringify({"Message": msgBox.val()}));
We are using JSON.stringify
to serialize the specified JSON object (containing just the Message
field) into a string, which is then sent to the server. Our Go code will decode (or unmarshal) the JSON string into a message
object, matching the field names from the client JSON object with those of our message
type.
Finally, update the socket.onmessage
callback function to expect JSON, and also add the name of the sender to the page:
socket.onmessage = function(e) { var msg = JSON.parse(e.data); messages.append( $("<li>").append( $("<strong>").text(msg.Name + ": "), $("<span>").text(msg.Message) ) ); }
In the preceding code snippet, we used JavaScript's JSON.parse
function to turn the JSON string into a JavaScript object and then access the fields to build up the elements needed to properly display them.
Build and run the application, and if you can, log in with two different accounts in two different browsers (or invite a friend to help you test it):
go build -o chat ./chat -host=":8080"
The following screenshot shows the chat application's browser chat screens:

- Delphi程序設計基礎:教程、實驗、習題
- Spring技術內(nèi)幕:深入解析Spring架構與設計
- vSphere High Performance Cookbook
- 編寫高質(zhì)量代碼:改善Python程序的91個建議
- R語言編程指南
- 假如C語言是我發(fā)明的:講給孩子聽的大師編程課
- 快速念咒:MySQL入門指南與進階實戰(zhàn)
- Gradle for Android
- Getting Started with NativeScript
- .NET 3.5編程
- 軟件品質(zhì)之完美管理:實戰(zhàn)經(jīng)典
- Learning R for Geospatial Analysis
- Spring Security Essentials
- Go語言開發(fā)實戰(zhàn)(慕課版)
- Visual Basic程序設計基礎