Creating Go Applications with App ID

5 min read

By: Eduardo Rodriguez

Integrate Go applications with IBM Cloud App ID by using OAuth 2

One problem that developers face when creating a new app is implementing their own sign-in and identity management mechanism. IBM Cloud App ID can help solve this problem! In this post, I will show you how to integrate Go applications with App ID by using OAuth 2. With this integration, you can use OIDC to retrieve user information when a user logs into your account.

What is App ID?

With IBM Cloud App ID, you can easily add authentication and authorization to your applications and APIs that run on IBM Cloud. With the service’s SDKs and APIs, developers can get a sign-in flow working in minutes, enable sign-in, and start building profiles on your app users. The user profile feature allows developers to aggregate and store information about their users that is provided by an identity provider or learned from their applications, such as preferences. In short, App ID enables your app to be used only by authorized users and ensures that those users have access only to what they should have access to. With App ID, your app experience can be professional, personalized, and, most importantly, secure.

App ID is OAuth 2 and OIDC compliant which allows any compliant authentication framework or SDK—such as Go—to easily integrate with App ID without any additional SDKs.

Sample app overview

The Go application we are going to create is defined as an OAuth 2 client application. This application requires a configuration JSON file that contains the required App ID configuration for this process to run. The main flow exposes a set of endpoints that are needed for the execution of the authorization grant code flowto obtain an access and identity token from App ID. As a result, both the user’s access token and profile are shown on the main home.html page.

Adding App ID to your app

Requirements

  • Have an instance of App ID

  • Install Go: https://golang.org/doc/install

  • Make sure your Go workspace exists in $HOME/go

Steps

*You can download the complete sample app here.

  1. Create a new project called appid.

  2. Create the App ID configuration file called appid_config.json under appid/config. The file should contain the following data:

    • ClientId: The identifier by which the OAuth 2 provider identifies your client.

    • ClientSecret: The associated secret.

    • AuthUrl: The URI to which the user is redirected to authorize access to the resource.

    • RedirectUrl: The callback URL where the flow is redirected after successfully logging in.

    {
      "ClientId" : "<clientId>",
      "ClientSecret" : "<clientSecret>",
      "AuthURL" : "<authURL>",
      "RedirectUrl" : "http://localhost:3000/auth/callback"
    }
  3. Create home.html under appid/static/(style and css files can be also added as needed), with the following content:

    {{ define "home" }}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{{.Title}}</title>
    
    </head>
    <body>
    
    {{if .User.Token}}
    Access Token: {{.User.Token}}
    <br>
    User Profile: {{.User.Profile}}
    <br>
    <a href="/logout" class="center-block" >Logout</a>
    {{else}}
    <a href="/login" class="center-block" >Login</a>
    {{end}}
    
    </body>
    </html>
    {{ end }}
  4. Create the main.go file under appid/ with the following content:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"github.com/tkanos/gonfig"
    	"golang.org/x/oauth2"
    	"log"
    	"net/http"
    	"html/template"
    	"os"
    	"path"
    	"runtime"
    	"strings"
    	"time"
    	"golang.org/x/net/context"
    	"errors"
    )
    
    var conf oauth2.Config
    const APP_ID_CONFIG = "/config/appid_config.json"
    const OPEN_ID_SCOPE = "openid"
    const PROFILE_SCOPE = "profile"
    const STATE = "state"
    const SESSION_TOKEN ="session_token"
    
    // Home struct, used for home.html template
    type Home struct{
    	Title string
    	User User
    }
    
    // User struct, holds all the user info shown in home.html
    type User struct {
    	Token string
    	Profile string
    }
    
    // App ID configuration struct
    type AppIdConfiguration struct {
    	ClientId string
    	ClientSecret string
    	AuthURL string
    	RedirectUrl string
    }
    
    // Builds a configuration object, with a given appidConfiguration struct
    func buildConfigurationObject(app_id_configuration AppIdConfiguration) oauth2.Config {
    
    	log.Println("Building configuration file.")
    
    	conf := &oauth2.Config{
    		ClientID: app_id_configuration.ClientId,
    		ClientSecret: app_id_configuration.ClientSecret,
    		RedirectURL: app_id_configuration.RedirectUrl,
    		Scopes: []string{OPEN_ID_SCOPE, PROFILE_SCOPE},
    		Endpoint:oauth2.Endpoint{
    			AuthURL: app_id_configuration.AuthURL+"/authorization",
    			TokenURL:  app_id_configuration.AuthURL+"/token",
    
    		},
    	}
    	return *conf
    }
    
    // Loads a configuration file, found in /config/appid_config.json
    func loadConfigurationFile() (AppIdConfiguration, error){
    
    	log.Println("Loading configuration file.")
    
    	app_id_configuration := AppIdConfiguration{}
    
    	// Using runtime.Caller, to make sure we get the path where the program is being executed
    	_, filename, _, ok := runtime.Caller(0)
    
    	if !ok{
    		return app_id_configuration, errors.New("Error calling runtime caller.")
    	}
    
    	// Reading configuration file
    	app_id_configuration_error := gonfig.GetConf(path.Dir(filename)+string(os.PathSeparator)+APP_ID_CONFIG, &app_id_configuration)
    
    	if app_id_configuration_error != nil {
    		return app_id_configuration, app_id_configuration_error
    	}
    
    	return app_id_configuration, nil
    }
    
    // Requests an OAuthToken using a "code" type
    func GetOauthToken(r *http.Request) (*oauth2.Token, error){
    
    	log.Println("Getting auth token.")
    
    	ctx := context.Background()
    
    	if ctx == nil{
    		return nil, errors.New("Could not get context.")
    	}
    
    	if r.URL.Query().Get(STATE) != STATE {
    		return nil, errors.New("State value did not match.")
    	}
    
    	// Exchange code for OAuth token
    	oauth2Token, oauth2TokenError := conf.Exchange(ctx, r.URL.Query().Get("code"))
    	if oauth2TokenError != nil {
    		return nil, errors.New("Failed to exchange token:"+ oauth2TokenError.Error())
    	}
    
    	return oauth2Token, nil
    }
    
    // Requests a user profile, using a bearer token
    func GetUserProfile(r *http.Request, token oauth2.Token) (interface{}, error){
    
    	log.Println("Getting user profile.")
    
    	ctx := context.Background()
    
    	if ctx == nil{
    		return nil, errors.New("Could not get context.")
    	}
    
    	// Getting now the userInfo
    	client := conf.Client(ctx, &token)
    
    	// Get request using /userinfo url
    	userinfoResponse, userinfoError := client.Get(strings.Replace(conf.Endpoint.AuthURL,"/authorization","/userinfo",1))
    	if userinfoError != nil {
    		return nil, errors.New("Failed to obtain userinfo:"+userinfoError.Error())
    	}
    
    	defer userinfoResponse.Body.Close()
    
    	// Decoding profile info and putting it in a map, to make it more readable
    	var profile map[string]interface{}
    	if userinfoError = json.NewDecoder(userinfoResponse.Body).Decode(&profile); userinfoError != nil {
    		return nil, userinfoError
    	}
    
    	return profile, nil
    
    }
    
    // Home handler for /home
    func home(w http.ResponseWriter, r *http.Request) {
    
    	log.Println("Executing /home")
    
    	// Parssing home.html template
    	tmpl,_ := template.ParseFiles("./static/home.html")
    	data := &Home{}
    
    	// Adding title to page
    	data.Title = "Welcome to AppID"
    
    	// Getting cookie named SESSION_TOKEN
    	cookie, err := r.Cookie(SESSION_TOKEN)
    
    	if err != nil{
    
    		// If no cookie found, that's ok, that means no user is logged in
    		log.Println("No session cookie found:"+err.Error())
    
    	}else {
    
    		log.Println("Session cookie found.")
    
    		// A cookie was found, this means a user is logged in
    		// Let's get the auth token value
    
    		authToken := oauth2.Token{
    			AccessToken: cookie.Value,
    		}
    
    		// Getting the user profile for the given auth token
    		profile, profileError := GetUserProfile(r, authToken)
    
    		if profileError != nil {
    			log.Print("Error getting profile.")
    		}
    
    		// Setting values in page template, this is what we are going to show for the logged in user
    		data.User.Token = fmt.Sprintln(authToken.AccessToken)
    		data.User.Profile = fmt.Sprintln(profile)
    
    		log.Println("User already logged in:" + fmt.Sprintln(profile))
    
    	}
    
    	tmpl.ExecuteTemplate(w, "home", data)
    
    }
    
    // Login handler for /login
    func login(w http.ResponseWriter, r *http.Request) {
    
    	log.Println("Executing /login")
    
    	// Code request to Auth URL
    	http.Redirect(w, r, conf.AuthCodeURL(STATE), http.StatusFound)
    
    }
    
    // Callback handler for /auth/callback
    func callback(w http.ResponseWriter, r *http.Request) {
    
    	log.Println("Executing /callback")
    
    	// Getting auth token from request
    	authToken, error := GetOauthToken(r)
    
    	if error != nil{
    
    		log.Println("Error getting auth token.")
    
    	}else {
    
    		log.Println("Setting session cookie.")
    
    		// Setting cookie with the value of this auth token
    
    		http.SetCookie(w, &http.Cookie{
    			Name:    "session_token",
    			Value:   authToken.AccessToken,
    			Path:    "/",
    			Expires: time.Now().Add(1000 * time.Second),
    		})
    
    	}
    
    	// Redirecting to /home, in order to show the logged in user values
    	http.Redirect(w, r, "/home",http.StatusSeeOther)
    
    }
    
    // Logout handler for /logout
    func logout(w http.ResponseWriter, r *http.Request) {
    
    	log.Println("Executing /logout")
    
    	// Getting session cookie
    	cookie, err := r.Cookie(SESSION_TOKEN)
    
    	if err != nil{
    
    		log.Println("No session cookie found:"+err.Error())
    
    	}else{
    
    		log.Println("Session cookie found, invalidating it.")
    
    		// If cookie was found, let's invalidate it
    		cookie.MaxAge = -1
    
    	}
    
    	// Setting the invalidated cookie
    	http.SetCookie(w, cookie)
    
    	// Redirecting to home for login screen
    	http.Redirect(w, r, "/home", http.StatusSeeOther)
    }
    
    func main() {
    
    	log.Println("Starting appid execution.")
    	// Loading App Id configuration file
    	app_id_configuration,  app_id_configuration_error := loadConfigurationFile()
    
    	if app_id_configuration_error != nil{
    
    		log.Println("Could not load configuration file.")
    
    	}
    
    	// Building global conf object, using App Id configuration
    	conf = buildConfigurationObject(app_id_configuration)
    
    	// Serving static files
    	fs := http.FileServer(http.Dir("static"))
    
    	// Creating handlers: /static /home /login /auth/callback /logout
    
    	http.Handle("/static/", http.StripPrefix("/static/",fs))
    
    	http.HandleFunc("/home", home)
    
    	http.HandleFunc("/login", login)
    
    	http.HandleFunc("/auth/callback", callback)
    
    	http.HandleFunc("/logout", logout)
    
    	// Using port 3000
    	port := ":3000"
    
    	log.Println("Listening on port ", port)
    
    	http.ListenAndServe(port, nil)
    
    }

Configuring App ID

App ID provides a default configuration social login with Google or Facebook. You can use the default configuration for the purposes of this blog, but the default is not intended to be used in production.

  1. Log in to your IBM Cloud account and navigate to your App ID dashboard. When there, go to Identity Providers -> Manage -> Authentication Settings, and add your redirect URL. This URL should be the same one that is in your appid_config.jsonfile. For example, if http://localhost:3000/auth/callbackis your web redirect URL, after App ID finishes the OAuth 2 process, it redirects your app to the provided URL.

    add web redirect URLs

  2. Be sure that your desired providers are On in Identity Providers -> Manage.

    Manage Identity Providers

  3. Navigate to the Service credentials tab and select the credentials entry. Click View credentials and copy the clientIdoauthServerUrl, and secret values. This information has to be added to the appid_config.json file.

    Service credentials

Executing the sample app

  1. Execute go run main.go from $HOME/go/src/appid.

  2. Open localhost:3000/home. This should show the main login page.

That’s it!

You’ve got a sample app up and running that you can customize to fit your needs. Great job!

We’d love to hear from you with feedback and questions. Get help for technical questions at Stack Overflow with the ibm-appid tag. For non-technical questions, use IBM developerWorks with the appid tag. For defect or support needs, use the support section in the IBM Cloud menu. To get started with App ID, check it out in the IBM Cloud Catalog.

Be the first to hear about news, product updates, and innovation from IBM Cloud