Compare commits

..

16 commits
v0.3.5 ... main

Author SHA1 Message Date
David Allen
e0a8d43421
Fixed token fetch from IDP 2024-07-01 12:29:31 -06:00
David Allen
a7e0e73e45
Added response body print to debug ID token 2024-07-01 12:29:31 -06:00
David Allen
8c01ba897f
Added verbose print to show ID and access tokens from IDP 2024-07-01 12:29:31 -06:00
David Allen
a0cca97e7d
Merge pull request #13 from opencube-horizon/bugfix/token-handler
server: fix error reporting and logic for /keys handler
2024-05-28 08:32:47 -06:00
Tiziano Müller
b304361ce9
server: fix error reporting and logic for /keys handler
restores proper error reporting to include the host dialed, and
fixes the tautological comparison `jwks == nil` in the recovery path
to properly refetch the server config and try again as intended
2024-05-27 10:28:53 +02:00
David J. Allen
e929fac09e
Fixed some minor issues 2024-04-30 16:03:23 -06:00
David J. Allen
7022801fe9
Implemented IDP registered clients and callbacks 2024-04-30 14:44:57 -06:00
David J. Allen
cbb3e6f851
Updated login page hint 2024-04-30 14:43:50 -06:00
David J. Allen
bc5e693425
Changed the IDP oauth endpoints to oauth2 2024-04-30 12:45:02 -06:00
David J. Allen
2edc624c01
Resetted the default IDP endpoint values 2024-04-30 12:28:19 -06:00
David J. Allen
e940dc2dd9
Fixed IDP endpoint overrides not working correctly 2024-04-30 11:32:28 -06:00
David J. Allen
67683e9fca
Fixed small issues with not building 2024-04-30 09:01:05 -06:00
David J. Allen
73e4e50d44
Made it possible to override certain example IDP endpoints 2024-04-29 18:45:30 -06:00
David J. Allen
f10a771db6
Added missing IDP function back 2024-04-29 14:58:51 -06:00
David J. Allen
c67c6f75a2
Added audience override for token sent to authorization server 2024-04-29 14:52:25 -06:00
20ba7bc735
Separated server and IDP code into different files 2024-04-28 13:23:25 -06:00
10 changed files with 405 additions and 314 deletions

View file

@ -2,6 +2,7 @@ package cmd
import ( import (
opaal "davidallendj/opaal/internal" opaal "davidallendj/opaal/internal"
"davidallendj/opaal/internal/oidc"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -9,9 +10,13 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var exampleCmd = &cobra.Command{ var (
endpoints oidc.Endpoints
)
var serveCmd = &cobra.Command{
Use: "serve", Use: "serve",
Short: "Start an simple identity provider server", Short: "Start an simple, bare minimal identity provider server",
Long: "The built-in identity provider is not (nor meant to be) a complete OIDC implementation and behaves like an external IdP", Long: "The built-in identity provider is not (nor meant to be) a complete OIDC implementation and behaves like an external IdP",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
s := opaal.NewServerWithConfig(&config) s := opaal.NewServerWithConfig(&config)
@ -21,11 +26,15 @@ var exampleCmd = &cobra.Command{
if errors.Is(err, http.ErrServerClosed) { if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("Identity provider server closed.\n") fmt.Printf("Identity provider server closed.\n")
} else if err != nil { } else if err != nil {
fmt.Errorf("failed to start server: %v", err) fmt.Printf("failed to start server: %v", err)
} }
}, },
} }
func init() { func init() {
rootCmd.AddCommand(exampleCmd) serveCmd.Flags().StringVar(&endpoints.Authorization, "endpoints.authorization", "", "set the authorization endpoint for the identity provider")
serveCmd.Flags().StringVar(&endpoints.Token, "endpoints.token", "", "set the token endpoint for the identity provider")
serveCmd.Flags().StringVar(&endpoints.JwksUri, "endpoints.jwks_uri", "", "set the JWKS endpoints for the identity provider")
rootCmd.AddCommand(serveCmd)
} }

View file

@ -2,6 +2,7 @@ package opaal
import ( import (
"davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -45,6 +46,7 @@ type TokenOptions struct {
Forwarding bool `yaml:"forwarding"` Forwarding bool `yaml:"forwarding"`
Refresh bool `yaml:"refresh"` Refresh bool `yaml:"refresh"`
Scope []string `yaml:"scope"` Scope []string `yaml:"scope"`
//TODO: allow specifying audience in returned token
} }
type Authentication struct { type Authentication struct {
@ -55,9 +57,10 @@ type Authentication struct {
} }
type Authorization struct { type Authorization struct {
Token TokenOptions `yaml:"token"`
Endpoints Endpoints `yaml:"endpoints"` Endpoints Endpoints `yaml:"endpoints"`
KeyPath string `yaml:"key-path"` KeyPath string `yaml:"key-path"`
Token TokenOptions `yaml:"token"` Audience []string `yaml:"audience"` // NOTE: overrides the "aud" claim in token sent to authorization server
} }
type Config struct { type Config struct {
@ -70,11 +73,18 @@ type Config struct {
} }
func NewConfig() Config { func NewConfig() Config {
return Config{ config := Config{
Version: goutil.GetCommit(), Version: goutil.GetCommit(),
Server: server.Server{ Server: server.Server{
Host: "127.0.0.1", Host: "127.0.0.1",
Port: 3333, Port: 3333,
Issuer: server.IdentityProviderServer{
Endpoints: oidc.Endpoints{
Authorization: "",
Token: "",
JwksUri: "",
},
},
}, },
Options: Options{ Options: Options{
RunOnce: true, RunOnce: true,
@ -97,6 +107,7 @@ func NewConfig() Config {
}, },
}, },
} }
return config
} }
func LoadConfig(path string) Config { func LoadConfig(path string) Config {

View file

@ -23,6 +23,7 @@ type JwtBearerFlowParams struct {
// IdentityProvider *oidc.IdentityProvider // IdentityProvider *oidc.IdentityProvider
TrustedIssuer *oauth.TrustedIssuer TrustedIssuer *oauth.TrustedIssuer
Client *oauth.Client Client *oauth.Client
Audience []string
Refresh bool Refresh bool
Verbose bool Verbose bool
KeyPath string KeyPath string
@ -50,6 +51,9 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
if client == nil { if client == nil {
return "", fmt.Errorf("invalid client (client is nil)") return "", fmt.Errorf("invalid client (client is nil)")
} }
if verbose {
fmt.Printf("ID token (IDP): %s\n access token (IDP): %s", accessToken, idToken)
}
if accessToken != "" { if accessToken != "" {
_, err := jws.Verify([]byte(accessToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true)) _, err := jws.Verify([]byte(accessToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true))
if err != nil { if err != nil {
@ -143,6 +147,11 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
payload["exp"] = time.Now().Add(time.Second * 3600 * 16).Unix() payload["exp"] = time.Now().Add(time.Second * 3600 * 16).Unix()
payload["sub"] = "opaal" payload["sub"] = "opaal"
// if an "audience" value is set, then override the token endpoint value
if len(params.Audience) > 0 {
payload["aud"] = params.Audience
}
// include the offline_access scope if refresh tokens are enabled // include the offline_access scope if refresh tokens are enabled
if params.Refresh { if params.Refresh {
v, ok := payload["scope"] v, ok := payload["scope"]

View file

@ -43,6 +43,7 @@ func Login(config *Config) error {
Issuer: config.Authorization.Endpoints.Issuer, Issuer: config.Authorization.Endpoints.Issuer,
Endpoints: oidc.Endpoints{ Endpoints: oidc.Endpoints{
Config: config.Authorization.Endpoints.Config, Config: config.Authorization.Endpoints.Config,
Authorization: config.Authorization.Endpoints.Authorize,
JwksUri: config.Authorization.Endpoints.JwksUri, JwksUri: config.Authorization.Endpoints.JwksUri,
}, },
}, },
@ -62,6 +63,7 @@ func Login(config *Config) error {
}, },
Verbose: config.Options.Verbose, Verbose: config.Options.Verbose,
Refresh: config.Authorization.Token.Refresh, Refresh: config.Authorization.Token.Refresh,
Audience: config.Authorization.Audience,
}, },
ClientCredentialsEndpoints: flows.ClientCredentialsFlowEndpoints{ ClientCredentialsEndpoints: flows.ClientCredentialsFlowEndpoints{
Clients: config.Authorization.Endpoints.Clients, Clients: config.Authorization.Endpoints.Clients,

View file

@ -90,9 +90,11 @@ func NewServerWithConfig(conf *Config) *server.Server {
}, },
Host: host, Host: host,
Port: port, Port: port,
Issuer: server.Issuer{ Issuer: server.IdentityProviderServer{
Host: conf.Server.Issuer.Host, Host: conf.Server.Issuer.Host,
Port: conf.Server.Issuer.Port, Port: conf.Server.Issuer.Port,
Endpoints: conf.Server.Issuer.Endpoints,
Clients: conf.Server.Issuer.Clients,
}, },
} }
return server return server

View file

@ -109,12 +109,14 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, state stri
} }
res, err := http.PostForm(client.Provider.Endpoints.Token, body) res, err := http.PostForm(client.Provider.Endpoints.Token, body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get ID token: %s", err) return nil, fmt.Errorf("failed to get ID token: %v", err)
} }
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
fmt.Printf("%s\n", string(b))
defer res.Body.Close() defer res.Body.Close()
// domain, _ := url.Parse("http://127.0.0.1") return b, nil
// client.Jar.SetCookies(domain, res.Cookies())
return io.ReadAll(res.Body)
} }

View file

@ -164,3 +164,25 @@ func (p *IdentityProvider) FetchJwks() error {
return nil return nil
} }
func (p *IdentityProvider) UpdateEndpoints(other *IdentityProvider) {
UpdateEndpoints(&p.Endpoints, &other.Endpoints)
}
func UpdateEndpoints(eps *Endpoints, other *Endpoints) {
// only update endpoints that are not empty
var UpdateIfEmpty = func(ep *string, s string) {
if ep != nil {
if *ep == "" {
*ep = s
}
}
}
UpdateIfEmpty(&eps.Config, other.Config)
UpdateIfEmpty(&eps.Authorization, other.Authorization)
UpdateIfEmpty(&eps.Token, other.Token)
UpdateIfEmpty(&eps.Revocation, other.Revocation)
UpdateIfEmpty(&eps.Introspection, other.Introspection)
UpdateIfEmpty(&eps.UserInfo, other.UserInfo)
UpdateIfEmpty(&eps.JwksUri, other.JwksUri)
}

290
internal/server/idp.go Normal file
View file

@ -0,0 +1,290 @@
package server
import (
"crypto/rand"
"crypto/rsa"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"net/http"
"os"
"slices"
"strings"
"time"
"github.com/davidallendj/go-utils/cryptox"
"github.com/davidallendj/go-utils/util"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
)
// TODO: make this a completely separate server
type IdentityProviderServer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Endpoints oidc.Endpoints `yaml:"endpoints"`
Clients []RegisteredClient `yaml:"clients"`
}
// NOTE: could we use a oauth.Client here instead??
type RegisteredClient struct {
Id string `yaml:"id"`
Secret string `yaml:"secret"`
Name string `yaml:"name"`
RedirectUris []string `yaml:"redirect-uris"`
}
func (s *Server) StartIdentityProvider() error {
// NOTE: this example does NOT implement CSRF tokens nor use them
// create an example identity provider
var (
r = chi.NewRouter()
// clients = []oauth.Client{}
activeCodes = []string{}
)
// update endpoints that have values set
defaultEps := oidc.Endpoints{
Authorization: "http://" + s.Addr + "/oauth2/authorize",
Token: "http://" + s.Addr + "/oauth2/token",
JwksUri: "http://" + s.Addr + "/.well-known/jwks.json",
}
oidc.UpdateEndpoints(&s.Issuer.Endpoints, &defaultEps)
// generate key pair used to sign JWKS and create JWTs
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return fmt.Errorf("failed to generate new RSA key: %v", err)
}
privateJwk, publicJwk, err := cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
if err != nil {
return fmt.Errorf("failed to generate JWK pair from private key: %v", err)
}
kid, _ := privateJwk.Get("kid")
publicJwk.Set("kid", kid)
publicJwk.Set("use", "sig")
publicJwk.Set("kty", "RSA")
publicJwk.Set("alg", "RS256")
if err := publicJwk.Validate(); err != nil {
return fmt.Errorf("failed to validate public JWK: %v", err)
}
// TODO: create .well-known JWKS endpoint with json
r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
// TODO: generate new JWKs from a private key
jwks := map[string]any{
"keys": []jwk.Key{
publicJwk,
},
}
b, err := json.Marshal(jwks)
if err != nil {
return
}
w.Write(b)
})
r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
// create config JSON to serve with GET request
config := map[string]any{
"issuer": "http://" + s.Addr,
"authorization_endpoint": s.Issuer.Endpoints.Authorization,
"token_endpoint": s.Issuer.Endpoints.Token,
"jwks_uri": s.Issuer.Endpoints.JwksUri,
"scopes_supported": []string{
"openid",
"profile",
"email",
},
"response_types_supported": []string{
"code",
},
"grant_types_supported": []string{
"authorization_code",
},
"id_token_signing_alg_values_supported": []string{
"RS256",
},
"claims_supported": []string{
"iss",
"sub",
"aud",
"exp",
"iat",
"name",
"email",
},
}
b, err := json.Marshal(config)
if err != nil {
return
}
w.Write(b)
})
r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
// serve up a simple login page
})
r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) {
// give consent for app to use
})
r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) {
// serve up a login page for user creds
form, err := os.ReadFile("pages/login.html")
if err != nil {
fmt.Printf("failed to load login form: %v", err)
}
w.Write(form)
})
r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) {
// check for example identity with POST request
r.ParseForm()
username := r.Form.Get("username")
password := r.Form.Get("password")
if len(s.Issuer.Clients) <= 0 {
fmt.Printf("no registered clients found with identity provider (add them in config)\n")
return
}
// example username and password so do simplified authorization code flow
if username == "openchami" && password == "openchami" {
client := s.Issuer.Clients[0]
// check if there are any redirect URIs supplied
if len(client.RedirectUris) <= 0 {
fmt.Printf("no redirect URIs found for client %s (ID: %s)\n", client.Name, client.Id)
return
}
for _, url := range client.RedirectUris {
// send an authorization code to each URI
code := util.RandomString(64)
activeCodes = append(activeCodes, code)
redirectUrl := fmt.Sprintf("%s?code=%s", url, code)
fmt.Printf("redirect URL: %s\n", redirectUrl)
http.Redirect(w, r, redirectUrl, http.StatusFound)
// _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil)
// if err != nil {
// fmt.Printf("failed to make request: %v\n", err)
// continue
// }
}
} else {
w.Write([]byte("error logging in"))
http.Redirect(w, r, "/browser/login", http.StatusUnauthorized)
}
})
r.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// check for authorization code and make sure it's valid
var code = r.Form.Get("code")
index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code })
if index < 0 {
fmt.Printf("invalid authorization code: %s\n", code)
return
}
// now create and return a JWT that can be verified with by authorization server
iat := time.Now().Unix()
exp := time.Now().Add(time.Second * 3600 * 16).Unix()
t := jwt.New()
t.Set(jwt.IssuerKey, s.Addr)
t.Set(jwt.SubjectKey, "ochami")
t.Set(jwt.AudienceKey, "ochami")
t.Set(jwt.IssuedAtKey, iat)
t.Set(jwt.ExpirationKey, exp)
t.Set("name", "ochami")
t.Set("email", "example@ochami.org")
t.Set("email_verified", true)
t.Set("scope", []string{
"openid",
"profile",
"email",
"example",
})
// payload := map[string]any{}
// payload["iss"] = s.Addr
// payload["aud"] = "ochami"
// payload["iat"] = iat
// payload["nbf"] = iat
// payload["exp"] = exp
// payload["sub"] = "ochami"
// payload["name"] = "ochami"
// payload["email"] = "example@ochami.org"
// payload["email_verified"] = true
// payload["scope"] = []string{
// "openid",
// "profile",
// "email",
// "example",
// }
payloadJson, err := json.MarshalIndent(t, "", "\t")
if err != nil {
fmt.Printf("failed to marshal payload: %v", err)
return
}
signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk))
if err != nil {
fmt.Printf("failed to sign token: %v\n", err)
return
}
// construct the bearer token with required fields
scope, _ := t.Get("scope")
bearer := map[string]any{
"token_type": "Bearer",
"id_token": string(signed),
"expires_in": exp,
"created_at": iat,
"scope": strings.Join(scope.([]string), " "),
}
b, err := json.MarshalIndent(bearer, "", "\t")
if err != nil {
fmt.Printf("failed to marshal bearer token: %v\n", err)
return
}
fmt.Printf("bearer: %s\n", string(b))
w.Write(b)
})
r.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
var (
responseType = r.URL.Query().Get("response_type")
clientId = r.URL.Query().Get("client_id")
redirectUris = r.URL.Query().Get("redirect_uri")
)
// check for required authorization code params
if responseType != "code" {
fmt.Printf("invalid response type\n")
return
}
// find a valid client
index := slices.IndexFunc(s.Issuer.Clients, func(c RegisteredClient) bool {
fmt.Printf("%s ? %s\n", c.Id, clientId)
return c.Id == clientId
})
if index < 0 {
fmt.Printf("no valid client found")
return
}
// TODO: check that our redirect URIs all match
for _, uri := range redirectUris {
_ = uri
}
// redirect to browser login since we don't do session management here
http.Redirect(w, r, "/browser/login", http.StatusFound)
})
s.Handler = r
return s.ListenAndServe()
}

View file

@ -1,28 +1,17 @@
package server package server
import ( import (
"crypto/rand"
"crypto/rsa"
"davidallendj/opaal/internal/flows" "davidallendj/opaal/internal/flows"
"davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc" "davidallendj/opaal/internal/oidc"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"slices" "slices"
"strings"
"time"
"github.com/davidallendj/go-utils/cryptox"
"github.com/davidallendj/go-utils/httpx" "github.com/davidallendj/go-utils/httpx"
"github.com/davidallendj/go-utils/util"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2"
"github.com/nikolalohinski/gonja/v2/exec" "github.com/nikolalohinski/gonja/v2/exec"
) )
@ -33,12 +22,7 @@ type Server struct {
Port int `yaml:"port"` Port int `yaml:"port"`
Callback string `yaml:"callback"` Callback string `yaml:"callback"`
State string `yaml:"state"` State string `yaml:"state"`
Issuer Issuer `yaml:"issuer"` Issuer IdentityProviderServer `yaml:"issuer"`
}
type Issuer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} }
type ServerParams struct { type ServerParams struct {
@ -73,7 +57,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
// make the login page SSO buttons and authorization URLs to write to stdout // make the login page SSO buttons and authorization URLs to write to stdout
buttons := "" buttons := ""
fmt.Printf("Login with external identity providers: \n") fmt.Printf("Login with an identity provider: \n")
for i, client := range clients { for i, client := range clients {
// fetch provider configuration before adding button // fetch provider configuration before adding button
p, err := oidc.FetchServerConfig(client.Provider.Issuer) p, err := oidc.FetchServerConfig(client.Provider.Issuer)
@ -90,8 +74,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
clients[i].Provider = *p clients[i].Provider = *p
buttons += makeButton(fmt.Sprintf("/login?sso=%s", client.Id), client.Name) buttons += makeButton(fmt.Sprintf("/login?sso=%s", client.Id), client.Name)
url := client.BuildAuthorizationUrl(s.State) fmt.Printf("\t%s: /login?sso=%s\n", client.Name, client.Id)
fmt.Printf("\t%s\n", url)
} }
var code string var code string
@ -131,7 +114,9 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
client = &clients[index] client = &clients[index]
url := client.BuildAuthorizationUrl(s.State) url := client.BuildAuthorizationUrl(s.State)
if params.Verbose {
fmt.Printf("Redirect URL: %s\n", url) fmt.Printf("Redirect URL: %s\n", url)
}
http.Redirect(w, r, url, http.StatusFound) http.Redirect(w, r, url, http.StatusFound)
return return
} }
@ -156,38 +141,47 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
p = params.AuthProvider p = params.AuthProvider
jwks []byte jwks []byte
) )
// try and get the JWKS from param first
if p.Endpoints.JwksUri != "" { fetchAndMarshal := func() (err error) {
err := p.FetchJwks() err = p.FetchJwks()
if err != nil { if err != nil {
fmt.Printf("failed to fetch keys using JWKS url...trying to fetch config and try again...\n") fmt.Printf("failed to fetch keys: %v\n", err)
return
} }
jwks, err = json.Marshal(p.KeySet) jwks, err = json.Marshal(p.KeySet)
if err != nil { if err != nil {
fmt.Printf("failed to marshal JWKS: %v\n", err) fmt.Printf("failed to marshal JWKS: %v\n", err)
} }
} else if p.Endpoints.Config != "" && jwks == nil {
// otherwise, try and fetch the whole config and try again
err := p.FetchServerConfig()
if err != nil {
fmt.Printf("failed to fetch server config: %v\n", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return return
} }
err = p.FetchJwks()
if err != nil { // try and get the JWKS from param first
fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err) if p.Endpoints.JwksUri != "" {
http.Redirect(w, r, "/error", http.StatusInternalServerError) if err := fetchAndMarshal(); err != nil {
w.Write(jwks)
return return
} }
} }
// forward the JWKS from the authorization server // otherwise or if fetching the JWKS failed, try and fetch the whole config first and try again
if jwks == nil { if p.Endpoints.Config != "" {
fmt.Printf("no JWKS was fetched from authorization server\n") if err := p.FetchServerConfig(); err != nil {
http.Redirect(w, r, "/error", http.StatusInternalServerError) fmt.Printf("failed to fetch server config: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
} else {
fmt.Printf("getting JWKS from param failed and endpoints config unavailable\n")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if err := fetchAndMarshal(); err != nil {
fmt.Printf("failed to fetch and marshal JWKS after config update: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Write(jwks) w.Write(jwks)
}) })
r.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
@ -339,256 +333,6 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
return s.ListenAndServe() return s.ListenAndServe()
} }
func (s *Server) StartIdentityProvider() error {
// NOTE: this example does NOT implement CSRF tokens nor use them
// create an example identity provider
var (
r = chi.NewRouter()
// clients = []oauth.Client{}
callback = ""
activeCodes = []string{}
)
// check if callback is set
if s.Callback == "" {
callback = "/oidc/callback"
}
// generate key pair used to sign JWKS and create JWTs
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return fmt.Errorf("failed to generate new RSA key: %v", err)
}
privateJwk, publicJwk, err := cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
if err != nil {
return fmt.Errorf("failed to generate JWK pair from private key: %v", err)
}
kid, _ := privateJwk.Get("kid")
publicJwk.Set("kid", kid)
publicJwk.Set("use", "sig")
publicJwk.Set("kty", "RSA")
publicJwk.Set("alg", "RS256")
if err := publicJwk.Validate(); err != nil {
return fmt.Errorf("failed to validate public JWK: %v", err)
}
// TODO: create .well-known JWKS endpoint with json
r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
// TODO: generate new JWKs from a private key
jwks := map[string]any{
"keys": []jwk.Key{
publicJwk,
},
}
b, err := json.Marshal(jwks)
if err != nil {
return
}
w.Write(b)
})
// TODO: create .well-known openid configuration
r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
// create config JSON to serve with GET request
config := map[string]any{
"issuer": "http://" + s.Addr,
"authorization_endpoint": "http://" + s.Addr + "/oauth/authorize",
"token_endpoint": "http://" + s.Addr + "/oauth/token",
"jwks_uri": "http://" + s.Addr + "/.well-known/jwks.json",
"scopes_supported": []string{
"openid",
"profile",
"email",
},
"response_types_supported": []string{
"code",
},
"grant_types_supported": []string{
"authorization_code",
},
"id_token_signing_alg_values_supported": []string{
"RS256",
},
"claims_supported": []string{
"iss",
"sub",
"aud",
"exp",
"iat",
"name",
"email",
},
}
b, err := json.Marshal(config)
if err != nil {
return
}
w.Write(b)
})
r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
// serve up a simple login page
})
r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) {
// give consent for app to use
})
r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) {
// serve up a login page for user creds
form, err := os.ReadFile("pages/login.html")
if err != nil {
fmt.Printf("failed to load login form: %v", err)
}
w.Write(form)
})
r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) {
// check for example identity with POST request
r.ParseForm()
username := r.Form.Get("username")
password := r.Form.Get("password")
// example username and password so do simplified authorization code flow
if username == "ochami" && password == "ochami" {
client := oauth.Client{
Id: "ochami",
Secret: "ochami",
Name: "ochami",
Provider: oidc.IdentityProvider{
Issuer: "http://127.0.0.1:3333",
},
RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)},
}
// check if there are any redirect URIs supplied
if len(client.RedirectUris) <= 0 {
fmt.Printf("no redirect URIs found")
return
}
for _, url := range client.RedirectUris {
// send an authorization code to each URI
code := util.RandomString(64)
activeCodes = append(activeCodes, code)
redirectUrl := fmt.Sprintf("%s?code=%s", url, code)
fmt.Printf("redirect URL: %s\n", redirectUrl)
http.Redirect(w, r, redirectUrl, http.StatusFound)
// _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil)
// if err != nil {
// fmt.Printf("failed to make request: %v\n", err)
// continue
// }
}
} else {
w.Write([]byte("error logging in"))
http.Redirect(w, r, "/browser/login", http.StatusUnauthorized)
}
})
r.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// check for authorization code and make sure it's valid
var code = r.Form.Get("code")
index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code })
if index < 0 {
fmt.Printf("invalid authorization code: %s\n", code)
return
}
// now create and return a JWT that can be verified with by authorization server
iat := time.Now().Unix()
exp := time.Now().Add(time.Second * 3600 * 16).Unix()
t := jwt.New()
t.Set(jwt.IssuerKey, s.Addr)
t.Set(jwt.SubjectKey, "ochami")
t.Set(jwt.AudienceKey, "ochami")
t.Set(jwt.IssuedAtKey, iat)
t.Set(jwt.ExpirationKey, exp)
t.Set("name", "ochami")
t.Set("email", "example@ochami.org")
t.Set("email_verified", true)
t.Set("scope", []string{
"openid",
"profile",
"email",
"example",
})
// payload := map[string]any{}
// payload["iss"] = s.Addr
// payload["aud"] = "ochami"
// payload["iat"] = iat
// payload["nbf"] = iat
// payload["exp"] = exp
// payload["sub"] = "ochami"
// payload["name"] = "ochami"
// payload["email"] = "example@ochami.org"
// payload["email_verified"] = true
// payload["scope"] = []string{
// "openid",
// "profile",
// "email",
// "example",
// }
payloadJson, err := json.MarshalIndent(t, "", "\t")
if err != nil {
fmt.Printf("failed to marshal payload: %v", err)
return
}
signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk))
if err != nil {
fmt.Printf("failed to sign token: %v\n", err)
return
}
// construct the bearer token with required fields
scope, _ := t.Get("scope")
bearer := map[string]any{
"token_type": "Bearer",
"id_token": string(signed),
"expires_in": exp,
"created_at": iat,
"scope": strings.Join(scope.([]string), " "),
}
b, err := json.MarshalIndent(bearer, "", "\t")
if err != nil {
fmt.Printf("failed to marshal bearer token: %v\n", err)
return
}
fmt.Printf("bearer: %s\n", string(b))
w.Write(b)
})
r.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
var (
responseType = r.URL.Query().Get("response_type")
clientId = r.URL.Query().Get("client_id")
redirectUris = r.URL.Query().Get("redirect_uri")
)
// check for required authorization code params
if responseType != "code" {
fmt.Printf("invalid response type\n")
return
}
// check that we're using the default registered client
if clientId != "ochami" {
fmt.Printf("invalid client\n")
return
}
// TODO: check that our redirect URIs all match
for _, uri := range redirectUris {
_ = uri
}
// redirect to browser login since we don't do session management here
http.Redirect(w, r, "/browser/login", http.StatusFound)
})
s.Handler = r
return s.ListenAndServe()
}
func makeButton(url string, text string) string { func makeButton(url string, text string) string {
// check if we have http:// a // check if we have http:// a
// html := "<input type=\"button\" " // html := "<input type=\"button\" "

View file

@ -7,7 +7,7 @@
<input type="password" id="password" name="password" title="password" placeholder="Enter your password..." /><br/> <input type="password" id="password" name="password" title="password" placeholder="Enter your password..." /><br/>
<button type="submit" class="btn">Login</button><br/> <button type="submit" class="btn">Login</button><br/>
<a class="forgot" href="#">Forgot Username?</a><br/> <a class="forgot" href="#">Forgot Username?</a><br/>
<label>(hint: try 'ochami' for both username and password)</label> <label>(hint: try 'openchami' for both username and password)</label>
</form> </form>
</div><!--end log form --> </div><!--end log form -->
</html> </html>