Skip to content

Commit

Permalink
Use OIDC library for fetching UserInfo
Browse files Browse the repository at this point in the history
The UserInfo endpoint for Okta is standards-compliant, so we should use
a standards-compliant library to access it
  • Loading branch information
punmechanic committed Nov 11, 2024
1 parent 1037f45 commit 7c56a6f
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 55 deletions.
16 changes: 9 additions & 7 deletions internal/api/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"strings"

"log/slog"

"golang.org/x/oauth2"
)

// RequestAttrs returns attributes to be used with slog for the given request.
Expand All @@ -28,16 +30,16 @@ func RequestAttrs(r *http.Request) []any {
return attrs
}

func GetBearerToken(r *http.Request) (string, bool) {
func requestTokenSource(r *http.Request) (oauth2.TokenSource, bool) {
headerValue := r.Header.Get("authorization")
if headerValue == "" {
return "", false
}

parts := strings.Split(headerValue, " ")
if len(parts) != 2 {
return "", false
return nil, false
}
if parts[0] != "Bearer" {
return nil, false
}

return parts[1], parts[0] == "Bearer"
token := oauth2.Token{AccessToken: parts[1]}
return oauth2.StaticTokenSource(&token), true
}
39 changes: 1 addition & 38 deletions internal/api/okta.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package api

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"

Expand Down Expand Up @@ -47,7 +44,7 @@ func (o Okta) ListApplicationsForUser(ctx context.Context, user string) ([]*okta
return links, nil
}

type OktaUserInfo struct {
type Claims struct {
Sub string `json:"sub"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Expand All @@ -61,37 +58,3 @@ var (
ErrBadRequest = errors.New("bad request")
ErrUnauthorized = errors.New("unauthorized")
)

// GetUserInfo returns user information about the given token
func (o Okta) GetUserInfo(ctx context.Context, token string) (info OktaUserInfo, err error) {
if o.client == nil {
o.client = http.DefaultClient
}

req, err := http.NewRequestWithContext(ctx, "GET", o.Domain.ResolveReference(&url.URL{Path: "/oauth2/v1/userinfo"}).String(), nil)
if err != nil {
return
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := o.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()

switch resp.StatusCode {
case http.StatusOK:
buf, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(buf, &info)
return
case http.StatusUnauthorized:
err = ErrUnauthorized
return
case http.StatusBadRequest:
err = ErrBadRequest
return
}

return
}
19 changes: 12 additions & 7 deletions internal/api/serverless_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"log/slog"

"github.com/coreos/go-oidc"
"github.com/okta/okta-sdk-golang/v2/okta"
)

Expand All @@ -16,25 +17,23 @@ type Application struct {
}

type OktaService interface {
GetUserInfo(ctx context.Context, token string) (OktaUserInfo, error)
ListApplicationsForUser(ctx context.Context, user string) ([]*okta.AppLink, error)
}

func ServeUserApplications(okta OktaService) http.Handler {
func ServeUserApplications(okta OktaService, idp *oidc.Provider) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestAttrs := RequestAttrs(r)
idToken, ok := GetBearerToken(r)
ts, ok := requestTokenSource(r)
if !ok {
slog.Error("no bearer token present", requestAttrs...)
ServeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}

info, err := okta.GetUserInfo(ctx, idToken)
info, err := idp.UserInfo(ctx, ts)
if err != nil {
if errors.Is(err, ErrBadRequest) {
// Something went wrong within Okta, and the user can't do anything about it.
slog.Error("okta indicated the request was poorly formed", requestAttrs...)
ServeJSONError(w, http.StatusInternalServerError, "internal error when talking to the Okta API")
return
Expand All @@ -46,8 +45,14 @@ func ServeUserApplications(okta OktaService) http.Handler {
return
}

requestAttrs = append(requestAttrs, slog.String("username", info.PreferredUsername))
applications, err := okta.ListApplicationsForUser(ctx, info.PreferredUsername)
var claims Claims
if err := info.Claims(&claims); err != nil {
slog.Error("failed to parse claims from Okta userinfo endpoint", requestAttrs...)
ServeJSONError(w, http.StatusInternalServerError, "internal error when talking to the Okta API")
}

requestAttrs = append(requestAttrs, slog.String("username", claims.PreferredUsername))
applications, err := okta.ListApplicationsForUser(ctx, claims.PreferredUsername)
if err != nil {
requestAttrs = append(requestAttrs, slog.String("error", err.Error()))
slog.Error("failed to fetch applications", requestAttrs...)
Expand Down
13 changes: 10 additions & 3 deletions lambda/list_applications/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (
"log/slog"

"github.com/aws/aws-lambda-go/lambda"
"github.com/coreos/go-oidc"
"github.com/riotgames/key-conjurer/internal"
"github.com/riotgames/key-conjurer/internal/api"
)

func main() {
settings, err := api.NewSettings(context.Background())
if err != nil {
slog.Error("could not fetch configuration: %s", err)
slog.Error("could not fetch configuration: %s", "error", err)
os.Exit(1)
}

Expand All @@ -24,7 +25,13 @@ func main() {
Host: settings.OktaHost,
}

slog.Info("running list_applications_v2 Lambda")
service := api.NewOktaService(&oktaDomain, settings.OktaToken)
lambda.StartHandler(internal.Lambdaify(api.ServeUserApplications(service)))
idp, err := oidc.NewProvider(context.Background(), oktaDomain.String())
if err != nil {
slog.Error("could not create OIDC provider", "error", err)
os.Exit(1)
}

slog.Info("running list_applications_v2 Lambda")
lambda.StartHandler(internal.Lambdaify(api.ServeUserApplications(service, idp)))
}

0 comments on commit 7c56a6f

Please sign in to comment.