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:

Logging in

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) to forward: make(chan *message)
  • Change r.tracer.Trace("Message received: ", string(msg)) to r.tracer.Trace("Message received: ", msg.Message)
  • Change send: make(chan []byte, messageBufferSize) to send: 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:

Augmenting messages with additional data

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.227.9