DEV Community

Ramu Mangalarapu
Ramu Mangalarapu

Posted on • Updated on

OpenID Connect with Okta in Golang

Hello,

If you do not have any idea about Authentication, Authorization, OAuth and OpenID Connect, please see my previous article about basic of IAM.

In this article, I am going to discuss simple steps how we can let the users authenticate with Okta and get the claims.

Note: This article strictly intended to the members who have OAuth, OpenID Connect and Golang knowledge.

Let's say your building application for company X whose data store and all IAM solutions are in Okta platform and now the new application your building for them should authenticate with Okta.

In such cases, we can do OAuth and fetch the tokens corresponding to the user.

We can use those tokens (claims, usually bearer access token and id token) and decode them to as per use case

Follow this to understand how we use Okta APIs for authentication.

Read OAuth2 Simplified for better understanding.

Here we are doing OpenID Connect to fetch user details and this means we are already authenticated the user with Okta.

Sounds confusing between OIDC and OAuth, yes, it is confusing but main difference you need to remembers is following things

1) OIDC is on top of OAuth2
2) OIDC can give you userinfo details
3) OIDC tells that user is authenticated

See here from Okta without fail: https://developer.okta.com/blog/2019/10/21/illustrated-guide-to-oauth-and-oidc

Let's write some code to explain OIDC.

Let's initialize Golang project by

go mod init example.com/okta-golang-oauth2-demo

This will create go.mod and sub sequent adding of packages will also create go.sum.

and create main.go, .env and oktaAPIs.go

Let's create required .env variables like below.

ISSUER_URL=YOUR_OKTA_DEV_DOMAIN/oauth2/default
CLIENT_ID=YOUR_OKTA_CLIENT_ID
CLIENT_SECRET=YOUR_OKTA_CLIENT_SECRET
REDIRECT_URI=http://localhost:9000/authorization/callback
SCOPE=openid profile email
NONCE=somenonce
STATE=somestate
RESPONSE_TYPE=code
AUTHORIZATION_CODE=authorization_code
Enter fullscreen mode Exit fullscreen mode

main.go


package main

import (
    "fmt"
    "log"
    "net/http"
    "net/url"
    "os"
    "text/template"

    "github.com/joho/godotenv"
)

var (
    // kept global because multiple functions can access them
    Issuer, ClientID, ClientSecret, RedirectURI, Scope, Nonce, State, ResponseType, AuthorizationCode string
)

func main() {
    //
    // Usually OAuth2.0 flow starts like below
    // First User (Resource Owner) tries to access client Application.
    // But Client Application checks the session if user is already logged in or not
    // If logged in, it will redirect the user request to page
    // If not user has to sign up using provided Sign In with options like Google, Facebook, Twitter etc..
    // Once your clicks on the "Login with X (Google)", it redirect the user to Authorization Server (here Google)
    // User enters creds and AS validates them, and Google asks the user, do you want this App
    // To read your profile, see your profile image, based on client requested scopes
    // And Google provides link to privacy policy and data usuage of the client App.
    // If resource owner (user) agrees to it, Google redirects to client redirection url.
    // Client takes the code and state from Google and verifies them, it exchanges them
    // fot tokens, Google gives some tokens about the user with all the scopes consented by the user.
    // Now client App can take decisions with the token in its hand of logged in user.

    // Loading env should be done first, because we are going to use
    // use the fields throught the program.
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    Issuer = os.Getenv("ISSUER_URL")
    ClientID = os.Getenv("CLIENT_ID")
    ClientSecret = os.Getenv("CLIENT_SECRET")
    RedirectURI = os.Getenv("REDIRECT_URI")
    Scope = os.Getenv("SCOPE")
    Nonce = os.Getenv("NONCE")
    State = os.Getenv("STATE")
    ResponseType = os.Getenv("RESPONSE_TYPE")
    AuthorizationCode = os.Getenv("AUTHORIZATION_CODE")

    // This is start of OAuth2.0 flow.
    // This is the home handler of our demo where we redirect the user to Okta UI
    http.HandleFunc("/", Index)

    // Now if Okta login is successful, Okta redirects to URL we specified in dashboard
    // we call this URL as Sign In Redirection URL.
    // This is usual OAuth2.0 callback handler process.
    // https://developer.okta.com/docs/reference/api/oidc/#authorize
    // We should check for 'code' and 'state' in query params.
    // And fetch tokens with the code.
    http.HandleFunc("/authorization/callback", Callback)

    log.Println("Starting the Go server at port :9000")
    log.Fatalf("Failed to start the server: %v\n", http.ListenAndServe(":9000", nil))
}

// Index generated Okta Authorization URL.
func Index(rw http.ResponseWriter, r *http.Request) {
    log.Println("Index of the Go Server")
    t, err := template.ParseFiles("index.html")
    if err != nil {
        fmt.Fprint(rw, fmt.Errorf("failed to parse 'index.html': %v", err).Error())
    }

    // This is hardcoded and manual :(.
    // Make sure you append "?" at first and "&" in between query params.
    // Alternatively you can also use http.NewRequest function to build request object
    // and then append query params using url.Values and encode them.
    // See my post: https://dev.to/ramu_mangalarapu/modifications-to-url-in-golang-4m80
    //
    // Now, OAuth2.0 flows start here, for more info, please see below URL.
    // See: https://developer.okta.com/docs/reference/api/oidc/#well-known-oauth-authorization-server
    authorizationURL := Issuer + "/v1/authorize?" + "client_id=" + ClientID + "&redirect_uri=" + RedirectURI + "&scope=" + Scope + "&nonce=" + Nonce + "&state=" + State + "&response_type=" + ResponseType
    log.Printf("Authorization Server URL is: %s", authorizationURL)
    if _, err := url.Parse(authorizationURL); err != nil {
        log.Fatalf("Invalid authorization URL: %s and error: %v", authorizationURL, err)
    }
    t.Execute(rw, authorizationURL)
}

// Callback is normal OAuth2.0 callback handler.
// Usually Authorization Server returns code, state and other params (in HTTP GET)
// to client App.
//
// With the received code and state, client App need to request for token.
func Callback(rw http.ResponseWriter, r *http.Request) {
    log.Println("Okta login is successful, we have got the code and state")
    authorizationServerReturnedState := r.URL.Query().Get("state")
    if authorizationServerReturnedState == "" {
        // So, let us fatal, as we cannot proceed
        log.Fatalf("Received empty 'state' or missing 'state' from Okta Authorization URL")
    }

    // let's compare if state returned by authorization server is matching the one
    // we sent initially, let's compare them
    if authorizationServerReturnedState != State {
        log.Printf("Expected %s: but got: %s", State, authorizationServerReturnedState)
        log.Fatalf("Received malformed 'state' from authorization server")
    }

    code := r.URL.Query().Get("code")
    if code == "" {
        fmt.Fprint(rw, "Received empty 'code' query param or missing 'code' from Authorization Server")
    }

    claims, err := fetchTokens(r, code)
    if err != nil {
        log.Fatalf("Failed to fetch tokens from Okta: %v", err)
    }

    log.Printf("Received response from Okta Token URL is :\n%v", claims)

    // this is to parse and represent the claims in bare html page.
    // or you could use those token, verify them or pass it other functions to let the flow
    // continue.
    t, err := template.ParseFiles("tokens.html")
    if err != nil {
        fmt.Fprint(rw, fmt.Errorf("failed to parse 'tokens.html': %v", err).Error())
    }

    // A different check but this would be useful if there is any
    // failure in calling API.
    if len(claims) == 0 {
        log.Fatal("Did not receive claims from Token endpoint")
    }

    // Check if key exists or not?
    rawJWT, exists := claims["access_token"]
    if !exists {
        log.Fatal("Could not find the key 'access_token' in Okta Token response")
    }

    // Check if it is of type "string" or not?
    jwt, isString := rawJWT.(string)
    if !isString {
        log.Fatalf("'access_token' expected to be string but found %T", jwt)
    }

    // Verifying the token to ensure it is received from Okta
    _, err = verifyAccessToken(jwt)
    if err != nil {
        log.Fatalf("Received token which is not signed By Okta: %v", err)
    }

    u, err := fetchUserInfo(jwt)
    if err != nil {
        log.Fatalf("Failed to fetch user details from Okta /userinfo URL: %v", err)
    }
    t.Execute(rw, u)
}


Enter fullscreen mode Exit fullscreen mode

oktaAPIs.go

package main

import (
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"

    verifier "github.com/okta/okta-jwt-verifier-golang"
)

func fetchTokens(r *http.Request, code string) (map[string]interface{}, error) {
    // Basic Authentication Header
    // See: https://en.wikipedia.org/wiki/Basic_access_authentication
    authHeader := base64.StdEncoding.EncodeToString(
        []byte(ClientID + ":" + ClientSecret))

    // Add required query params
    q := r.URL.Query()
    q.Add("grant_type", AuthorizationCode)
    q.Set("code", code)
    q.Add("redirect_uri", RedirectURI)

    // Token URL of Okta and how we need to call it.
    // Please see:https://developer.okta.com/docs/reference/api/oidc/#token
    url := Issuer + "/v1/token?" + q.Encode()
    req, err := http.NewRequest(http.MethodPost, url, nil)
    if err != nil {
        return nil, fmt.Errorf("could not build new HTTP Request object: %v", err)
    }

    h := req.Header
    h.Add("Authorization", "Basic "+authHeader)
    h.Add("Accept", "application/json")
    h.Add("Content-Type", "application/x-www-form-urlencoded")
    h.Add("Connection", "close")
    h.Add("Content-Length", "0")

    log.Printf("Okta Token URL is: %s", req.URL.String())
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("could not do HTTP POST request to Okta Token URL: %v", err)
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("could not read HTTP Response body from Okta Token URL: %v", err)
    }
    // Alawys close response body
    defer resp.Body.Close()

    // why map, becuase it similarly represents the JSON in Golang :)
    claims := make(map[string]interface{})
    json.Unmarshal(body, &claims)
    return claims, nil
}

// fetchUserInfo is to fetch user details by access token.
// Please see: https://developer.okta.com/docs/reference/api/oidc/#userinfo
func fetchUserInfo(token string) (map[string]string, error) {
    userInfoURL := Issuer + "/v1/userinfo"
    if _, err := url.Parse(userInfoURL); err != nil {
        return nil, fmt.Errorf("invalid Okta User Info URL: %s")
    }

    req, err := http.NewRequest(http.MethodPost, userInfoURL, nil)
    if err != nil {
        return nil, fmt.Errorf("Failed to create request object for Okta /userinfo endpoint: %v", err)
    }

    req.Header.Add("Authorization", "Bearer "+token)
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("Failed to send HTTP POST request to Okta /userinfo endpoint: %v", err)
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("Failed to read response body from Okta /userinfo endpoint: %v", err)
    }
    defer resp.Body.Close()

    userDetails := make(map[string]string)
    json.Unmarshal(body, &userDetails)
    return userDetails, nil
}

func verifyAccessToken(t string) (*verifier.Jwt, error) {
    toValidate := map[string]string{}
    toValidate["nonce"] = Nonce
    toValidate["aud"] = "api://default" // why this right? so, please see: https://github.com/okta/okta-jwt-verifier-golang#usage

    jV := verifier.JwtVerifier{
        Issuer:           Issuer,
        ClaimsToValidate: toValidate,
    }

    result, err := jV.New().VerifyAccessToken(t)
    if err != nil {
        log.Println("Invalid Token")
        return nil, fmt.Errorf("%s", err)
    }

    if result != nil {
        log.Println("Valid token")
        return result, nil
    }
    return nil, fmt.Errorf("token could not be verified: %s", "")
}

Enter fullscreen mode Exit fullscreen mode

Now, you can run

go run .\main.go .\oktaAPIs.go to see the UI of link, click on the link and it will redirect you to Okta UI.

Make sure you assign users to this particular App and login with that user, it will fetch user details about him.

Here one more important thing is, please use your environment variables properly, make sure redirect URI specified in Okta Developer dashboard is same as the one your building it.

I will be sharing more of my knowledge in coming articles.

Thanks for reading.
Happy Coding :)

Discussion (1)

Collapse
ynd profile image
Narendra

Hi Ram .. Im getting panic error while using above code
2022/03/14 17:16:30 http: panic serving [::1]:50494: runtime error: invalid memory address or nil pointer dereference
goroutine 6 [running]:
net/http.(*conn).serve.func1(

Please help me