Skip to content

Commit

Permalink
Add userinfo endpoint
Browse files Browse the repository at this point in the history
Serve up some info about the user, including an avatar. This will be used by
various services to collect profile info.
  • Loading branch information
lstoll committed Mar 9, 2024
1 parent ef35bc2 commit c85df53
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 15 deletions.
34 changes: 19 additions & 15 deletions e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ func TestE2E(t *testing.T) {
t.Fatal("server startup timed out")
}

provider, err := oidc.DiscoverProvider(ctx, issConfig.URL.String(), nil)
if err != nil {
t.Fatal(err)
}
oa2Cfg := oauth2.Config{
ClientID: "test-cli",
ClientSecret: "public",
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID},
}

// not strictly needed for the E2E tests.
reloadDB(net.JoinHostPort("localhost", port))

Expand Down Expand Up @@ -213,7 +224,7 @@ func TestE2E(t *testing.T) {
clearErrchan(chromeErrC)

testOk = t.Run("Successful Login", func(t *testing.T) {
tokC, loginErrC := cliLoginFlow(ctx, t, issConfig.URL.String())
tokC, loginErrC := cliLoginFlow(ctx, t, provider, oa2Cfg)

runErrC := make(chan error, 1)
doneC := make(chan struct{}, 1)
Expand All @@ -229,6 +240,11 @@ func TestE2E(t *testing.T) {

select {
case tok := <-tokC:
ui, err := provider.Userinfo(ctx, oa2Cfg.TokenSource(ctx, tok))
if err != nil {
t.Fatalf("getting userinfo: %v", err)
}
t.Logf("userinfo: %v", ui)
// positive case
//
// TODO(lstoll) get userinfo
Expand Down Expand Up @@ -257,7 +273,7 @@ func TestE2E(t *testing.T) {
t.Fatal(err)
}

tokC, errC := cliLoginFlow(ctx, t, issConfig.URL.String())
tokC, errC := cliLoginFlow(ctx, t, provider, oa2Cfg)

runErrC := make(chan error, 1)
doneC := make(chan struct{}, 1)
Expand Down Expand Up @@ -305,21 +321,9 @@ func TestE2E(t *testing.T) {
// If an error occurs, that will be returned on that channel. It is the callers
// responsibility to complete the flow - this will only get you to the initial
// URL for the flow.
func cliLoginFlow(ctx context.Context, t *testing.T, issuer string) (chan *oauth2.Token, chan error) { //nolint:thelper // it's not that kind of helper
func cliLoginFlow(ctx context.Context, t *testing.T, provider *oidc.Provider, oa2Cfg oauth2.Config) (chan *oauth2.Token, chan error) { //nolint:thelper // it's not that kind of helper
openCh := make(chan struct{}, 1)

provider, err := oidc.DiscoverProvider(ctx, issuer, nil)
if err != nil {
t.Fatal(err)
}

oa2Cfg := oauth2.Config{
ClientID: "test-cli",
ClientSecret: "public",
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID},
}

cli, err := clitoken.NewSource(ctx, oa2Cfg, clitoken.WithOpener(&chromeDPOpener{notifyCh: openCh}))
if err != nil {
t.Fatal(err)
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/lstoll/cookiesession"
"github.com/lstoll/oidc"
"github.com/lstoll/oidc/core"
"github.com/lstoll/oidc/core/staticclients"
"github.com/lstoll/oidc/discovery"
Expand Down Expand Up @@ -149,6 +150,8 @@ func serve(ctx context.Context, db *DB, issuer issuerConfig, addr string) error
oidcmd := discovery.DefaultCoreMetadata(issuer.URL.String())
oidcmd.AuthorizationEndpoint = issuer.URL.String() + "/auth"
oidcmd.TokenEndpoint = issuer.URL.String() + "/token"
oidcmd.ScopesSupported = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, "offline"}
oidcmd.UserinfoEndpoint = issuer.URL.String() + "/userinfo"

discoh, err := discovery.NewConfigurationHandler(oidcmd, oidcHandles)
if err != nil {
Expand Down
45 changes: 45 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package main

import (
"crypto/md5"
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"log/slog"
"net/http"
"strings"
"time"

"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/lstoll/cookiesession"
"github.com/lstoll/oidc"
"github.com/lstoll/oidc/core"
)

Expand Down Expand Up @@ -119,6 +123,7 @@ func (s *oidcServer) AddHandlers(mux *http.ServeMux) {
mux.HandleFunc("/start", s.startLogin)
mux.HandleFunc("/finish", s.finishLogin)
mux.HandleFunc("/loggedin", s.loggedIn)
mux.HandleFunc("GET /userinfo", s.userinfo)
}

func (s *oidcServer) startLogin(rw http.ResponseWriter, req *http.Request) {
Expand Down Expand Up @@ -266,8 +271,48 @@ func (s *oidcServer) loggedIn(rw http.ResponseWriter, req *http.Request) {
_ = s.oidcsvr.FinishAuthorization(rw, req, authdUser.SessionID, az)
}

func (s *oidcServer) userinfo(w http.ResponseWriter, req *http.Request) {
err := s.oidcsvr.Userinfo(w, req, func(w io.Writer, uireq *core.UserinfoRequest) error {
u, err := s.db.GetUserByID(uireq.Subject)
if err != nil {
return fmt.Errorf("getting user %s: %w", uireq.Subject, err)
}

// TODO(lstoll) pass through the scopes, use them to decide what to
// reurn. For now all info is good enough, we don't have a consent
// process anyway.
//
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
cl := oidc.IDClaims{
Issuer: s.issuer,
Subject: uireq.Subject,
Extra: make(map[string]any),
}
cl.Extra["email"] = u.Email
cl.Extra["email_verified"] = true
cl.Extra["picture"] = gravatarURL(u.Email) // thank u tom
cl.Extra["name"] = u.FullName
nsp := strings.Split(u.FullName, " ")
if len(nsp) == 2 {
cl.Extra["given_name"] = nsp[0]
cl.Extra["family_name"] = nsp[1]
}
// cl.Extra["preferred_username"] TODO(lstoll) just e-email? do we want a username field?

return json.NewEncoder(w).Encode(cl)
})
if err != nil {
s.eh.Error(w, req, err)
}
}

func (s *oidcServer) httpErr(rw http.ResponseWriter, err error) {
// TODO - replace me with the error handler
slog.Error("(TODO improve this handler) error in server", logErr(err))
http.Error(rw, "Internal Error", http.StatusInternalServerError)
}

func gravatarURL(email string) string {
hash := md5.Sum([]byte(email))
return fmt.Sprintf("https://www.gravatar.com/avatar/%x.png", hash)
}

0 comments on commit c85df53

Please sign in to comment.