mirror of
https://github.com/davidallendj/opaal.git
synced 2026-02-04 08:46:27 -07:00
Compare commits
No commits in common. "main" and "v0.3.5" have entirely different histories.
10 changed files with 313 additions and 404 deletions
17
cmd/serve.go
17
cmd/serve.go
|
|
@ -2,7 +2,6 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
opaal "davidallendj/opaal/internal"
|
opaal "davidallendj/opaal/internal"
|
||||||
"davidallendj/opaal/internal/oidc"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -10,13 +9,9 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var exampleCmd = &cobra.Command{
|
||||||
endpoints oidc.Endpoints
|
|
||||||
)
|
|
||||||
|
|
||||||
var serveCmd = &cobra.Command{
|
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Start an simple, bare minimal identity provider server",
|
Short: "Start an simple 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)
|
||||||
|
|
@ -26,15 +21,11 @@ var serveCmd = &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.Printf("failed to start server: %v", err)
|
fmt.Errorf("failed to start server: %v", err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
serveCmd.Flags().StringVar(&endpoints.Authorization, "endpoints.authorization", "", "set the authorization endpoint for the identity provider")
|
rootCmd.AddCommand(exampleCmd)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package opaal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"davidallendj/opaal/internal/oauth"
|
"davidallendj/opaal/internal/oauth"
|
||||||
"davidallendj/opaal/internal/oidc"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -46,7 +45,6 @@ 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 {
|
||||||
|
|
@ -57,10 +55,9 @@ 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"`
|
||||||
Audience []string `yaml:"audience"` // NOTE: overrides the "aud" claim in token sent to authorization server
|
Token TokenOptions `yaml:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
@ -73,18 +70,11 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() Config {
|
func NewConfig() Config {
|
||||||
config := Config{
|
return 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,
|
||||||
|
|
@ -107,7 +97,6 @@ func NewConfig() Config {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) Config {
|
func LoadConfig(path string) Config {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ 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
|
||||||
|
|
@ -51,9 +50,6 @@ 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 {
|
||||||
|
|
@ -147,11 +143,6 @@ 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"]
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,8 @@ func Login(config *Config) error {
|
||||||
AuthProvider: &oidc.IdentityProvider{
|
AuthProvider: &oidc.IdentityProvider{
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{
|
JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{
|
||||||
|
|
@ -61,9 +60,8 @@ func Login(config *Config) error {
|
||||||
ExpiresAt: time.Now().Add(config.Authorization.Token.Duration),
|
ExpiresAt: time.Now().Add(config.Authorization.Token.Duration),
|
||||||
Scope: []string{},
|
Scope: []string{},
|
||||||
},
|
},
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,9 @@ func NewServerWithConfig(conf *Config) *server.Server {
|
||||||
},
|
},
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
Issuer: server.IdentityProviderServer{
|
Issuer: server.Issuer{
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -109,14 +109,12 @@ 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: %v", err)
|
return nil, fmt.Errorf("failed to get ID token: %s", 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()
|
||||||
|
|
||||||
return b, nil
|
// domain, _ := url.Parse("http://127.0.0.1")
|
||||||
|
// client.Jar.SetCookies(domain, res.Cookies())
|
||||||
|
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,25 +164,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +1,44 @@
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*http.Server
|
*http.Server
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
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 IdentityProviderServer `yaml:"issuer"`
|
Issuer Issuer `yaml:"issuer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Issuer struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerParams struct {
|
type ServerParams struct {
|
||||||
|
|
@ -57,7 +73,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 an identity provider: \n")
|
fmt.Printf("Login with external identity providers: \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)
|
||||||
|
|
@ -74,7 +90,8 @@ 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)
|
||||||
fmt.Printf("\t%s: /login?sso=%s\n", client.Name, client.Id)
|
url := client.BuildAuthorizationUrl(s.State)
|
||||||
|
fmt.Printf("\t%s\n", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
var code string
|
var code string
|
||||||
|
|
@ -114,9 +131,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
@ -141,47 +156,38 @@ 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
|
||||||
fetchAndMarshal := func() (err error) {
|
if p.Endpoints.JwksUri != "" {
|
||||||
err = p.FetchJwks()
|
err := p.FetchJwks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to fetch keys: %v\n", err)
|
fmt.Printf("failed to fetch keys using JWKS url...trying to fetch config and try again...\n")
|
||||||
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)
|
||||||
}
|
}
|
||||||
return
|
} else if p.Endpoints.Config != "" && jwks == nil {
|
||||||
}
|
// otherwise, try and fetch the whole config and try again
|
||||||
|
err := p.FetchServerConfig()
|
||||||
// try and get the JWKS from param first
|
if err != nil {
|
||||||
if p.Endpoints.JwksUri != "" {
|
|
||||||
if err := fetchAndMarshal(); err != nil {
|
|
||||||
w.Write(jwks)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise or if fetching the JWKS failed, try and fetch the whole config first and try again
|
|
||||||
if p.Endpoints.Config != "" {
|
|
||||||
if err := p.FetchServerConfig(); err != nil {
|
|
||||||
fmt.Printf("failed to fetch server config: %v\n", err)
|
fmt.Printf("failed to fetch server config: %v\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = p.FetchJwks()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err)
|
||||||
|
http.Redirect(w, r, "/error", 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 {
|
// forward the JWKS from the authorization server
|
||||||
fmt.Printf("failed to fetch and marshal JWKS after config update: %v\n", err)
|
if jwks == nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
fmt.Printf("no JWKS was fetched from authorization server\n")
|
||||||
|
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
||||||
return
|
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) {
|
||||||
|
|
@ -333,6 +339,256 @@ 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\" "
|
||||||
|
|
|
||||||
|
|
@ -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 'openchami' for both username and password)</label>
|
<label>(hint: try 'ochami' for both username and password)</label>
|
||||||
</form>
|
</form>
|
||||||
</div><!--end log form -->
|
</div><!--end log form -->
|
||||||
</html>
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue