Clone this repo, add your dist/ folder to the root directory and run the following commands after setting up your integration in Okta.
# Add environment variables
export ROOT_URL=http://localhost:3000
export IDP_METADATA_URL=<Okta Metadata URL>
# Create certs
./scripts/cert.sh
# Run the server
go run cmd/server.go
Create an Okta SAML Integration with Okta admin
- Open your Okta Admin dashboard, on the left pane select applications, create new integration
- Create a SAML integration
- Name your application
- Set SSO URL and Audience URI with our local configuration
- Set Name ID Format to EmailAddress
- Set attribute statements
Add Environment variables
ROOT_URL=http://localhost:3000
IDP_METADATA_URL=<Okta Metadata URL>
Create ssl certs for the application.
openssl req -x509 -newkey rsa:2048 -keyout okta-app.key -out okta-app.cert -days 365 -nodes -subj "/CN=example.com"
Start building Our final product will look like this:
.
├── cmd
│ └── server.go
├── go.mod
├── go.sum
├── pkg
│ ├── app
│ │ ├── api.go
│ │ ├── info.go
│ │ └── ui.go
│ └── core
│ ├── configs
│ │ └── configs.go
│ ├── middleware
│ │ └── saml.go
│ └── models
│ └── user.go
└── scripts
└── cert.sh
Let's begin by setting up our project
mkdir okta_go_saml && cd okta_go_saml
go mod init github.com/<your github username>/okta_go_saml
Next, lay down the structure for our application.
mkdir cmd
mkdir pkg && cd pkg
mkdir app
mkdir core && cd core
mkdir configs
mkdir middleware
mkdir models
I like to start with the core directory, create the following files:
- configs/configs.go When we need any environment variables, we'll grab them from the configs package. Note these will use variables from a .env file if present in the root directory.
package configs
import (
"log"
"os"
"github.com/joho/godotenv"
)
func RootUrl() string {
err := godotenv.Load()
if err != nil {
log.Println("No .env file found")
}
return os.Getenv("ROOT_URL")
}
func IdpMetadataUrl() string {
err := godotenv.Load()
if err != nil {
log.Println("No .env file found")
}
return os.Getenv("IDP_METADATA_URL")
}
- models/users.go We'll create a model for Users for use in the frontend if needed. In our server, we'll grab the SAML assertion data (email, firstName, lastName that we added in Okta) marshal to JSON and send back to the client.
package models
type User struct {
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}
- middleware/saml.go Here we set up the middleware that actually handles the SAML authentication. This is made simple with the library github.com/crewjam/saml/samlsp
package middleware
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"log"
"net/http"
"net/url"
"github.com/crewjam/saml/samlsp"
"github.com/eddique/okta_go_saml/pkg/core/configs"
)
func SamlMiddleware() (*samlsp.Middleware, error) {
// Load our certs
keyPair, err := tls.LoadX509KeyPair("okta-app.cert", "okta-app.key")
if err != nil {
log.Fatalln("Fatal Error:", err)
}
// Parse certificate to x509
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
log.Fatalln("Fatal Error:", err)
}
// Set the IDP metadata url
idpMetadataURL, err := url.Parse(configs.IdpMetadataUrl())
if err != nil {
log.Fatalln("Fatal Error:", err)
}
// Fetch metadata on request
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient,
*idpMetadataURL)
if err != nil {
log.Fatalln("Fatal Error:", err)
}
// Set the apps root URL (http://localhost:3000 in this example)
rootURL, err := url.Parse(configs.RootUrl())
if err != nil {
log.Fatalln("Fatal Error:", err)
}
// Create the SAML middleware
samlSP, err := samlsp.New(samlsp.Options{
URL: *rootURL,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
IDPMetadata: idpMetadata,
AllowIDPInitiated: true,
})
return samlSP, err
}
And run the following command:
go mod tidy
Create handlers for server endpoints. In okta_go_saml/pkg/app, create the following files:
- app/api.go Begin with a struct that will manage our endpoints and a simple health endpoint handler.
package app
import (
"net/http"
)
type ApiAdapter struct{}
func NewAPIAdapter() *ApiAdapter {
return &ApiAdapter{}
}
func (api ApiAdapter) Health(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
- app/info.go Our info handler to send user data from the SAML session to the client
package app
import (
"encoding/json"
"log"
"net/http"
"github.com/crewjam/saml/samlsp"
"github.com/eddique/okta_go_saml/pkg/core/models"
)
func (api *ApiAdapter) InfoHandler(w http.ResponseWriter, r *http.Request) {
// Get user attributes from SAML session
email := samlsp.AttributeFromContext(r.Context(), "email")
firstName := samlsp.AttributeFromContext(r.Context(), "firstName")
lastName := samlsp.AttributeFromContext(r.Context(), "lastName")
// Create user struct
user := models.User{
Email: email,
FirstName: firstName,
LastName: lastName,
}
// Marshal struct to JSON
jsonData, err := json.Marshal(user)
if err != nil {
log.Println("Error:", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Respond with user data
w.Write(jsonData)
}
- app/ui.go Create a handler to serve our static dist/ folder
package app
import (
"net/http"
"os"
)
func (api *ApiAdapter) FileServer(w http.ResponseWriter, r *http.Request) {
path := "./dist" + r.URL.Path
if _, err := os.Stat(path); os.IsNotExist(err) {
http.ServeFile(w, r, "./dist/index.html")
} else {
http.ServeFile(w, r, path)
}
}
Create the main function for our server. Create server.go in the cmd/ directory. cmd/server.go
package main
import (
"log"
"net/http"
"github.com/eddique/okta_go_saml/pkg/app"
"github.com/eddique/okta_go_saml/pkg/core/middleware"
"github.com/gorilla/mux"
)
func main() {
api := app.NewAPIAdapter()
samlSP, err := middleware.SamlMiddleware()
if err != nil {
log.Fatalln(err)
}
router := mux.NewRouter()
router.PathPrefix("/api/v1/whoami").HandlerFunc(api.InfoHandler)
router.PathPrefix("/").Handler(http.HandlerFunc(api.FileServer))
app := samlSP.RequireAccount(router)
http.Handle("/", samlSP.RequireAccount(app))
http.Handle("/saml/", samlSP)
http.ListenAndServe(":3000", nil)
}
and run go mod tidy once more
go mod tidy
Run the server locally and access from the Okta application.
go run cmd/server.go