Compare commits

...

2 Commits

Author SHA1 Message Date
805bfe56db Can now run as a Ory Hydra login and consent app 2023-11-03 18:18:25 +01:00
6498a8bd06 Successfully fetch login status 2023-10-27 01:17:30 +02:00
15 changed files with 405 additions and 38 deletions

70
app/ipasso/cookies.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"context"
"git.thequux.com/thequux/ipasso/backend"
"go.uber.org/zap"
"net/http"
)
type loginStateMw struct {
handler http.Handler
}
type Session struct {
State LoginState
Session *backend.Session
Cache *backend.SessionCache
}
func (l loginStateMw) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
var cookie *http.Cookie
var session Session
session.State = LS_Unknown
for _, cookie = range req.Cookies() {
// TODO: make host configurable
if cookie.Name != "IPASSO_SID" {
continue
}
zap.L().Debug("Session ID found in req", zap.String(cookie.Name, cookie.Value))
sid := cookie.Value
if sid == "[logout]" {
session.State = LS_LoggedOut
break
}
bsession, bcache, err := backend.SessionStore.GetSession(ctx, sid)
if err != nil {
zap.L().Warn("Failed to fetch cookie", zap.Error(err))
// cookie was invalid
session.State = LS_Invalid
} else {
session.State = LS_Valid
session.Session = &bsession
session.Cache = bcache
}
break
}
ctx = context.WithValue(ctx, "ipa.Session", &session)
l.handler.ServeHTTP(w, req.WithContext(ctx))
}
func GetSession(ctx context.Context, loadCache bool) *Session {
session := ctx.Value("ipa.Session").(*Session)
if session != nil && session.Session != nil && loadCache && (session.Cache == nil || !session.Cache.Valid) {
cache, err := buildSessionCache(session.Session, nil)
if err != nil {
zap.L().Warn("Failed to build session cache", zap.Error(err))
} else {
session.Cache = &cache
}
} else {
zap.L().Debug("Not rebuilding session cache", zap.Any("session", session))
}
return session
}
func LoginStateMw(handler http.Handler) http.Handler {
return loginStateMw{handler}
}

View File

@@ -205,7 +205,7 @@ func buildSessionCache(b *backend.Session, entry *ldap.Entry) (cache backend.Ses
} else if attr.Name == "givenName" && len(attr.Values) > 0 { } else if attr.Name == "givenName" && len(attr.Values) > 0 {
cache.GivenName = strings.Join(attr.Values, " ") cache.GivenName = strings.Join(attr.Values, " ")
} else if attr.Name == "sn" { } else if attr.Name == "sn" {
cache.SurName = strings.Join(attr.Values, " ") cache.FamilyName = strings.Join(attr.Values, " ")
} else if attr.Name == "mail" && len(attr.Values) > 0 { } else if attr.Name == "mail" && len(attr.Values) > 0 {
cache.Email = attr.Values[0] cache.Email = attr.Values[0]
} else if attr.Name == "memberOf" { } else if attr.Name == "memberOf" {

View File

@@ -2,10 +2,15 @@ package main
import ( import (
"errors" "errors"
"flag"
"git.thequux.com/thequux/ipasso/backend" "git.thequux.com/thequux/ipasso/backend"
"git.thequux.com/thequux/ipasso/resources"
"git.thequux.com/thequux/ipasso/util/startup" "git.thequux.com/thequux/ipasso/util/startup"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
hydra "github.com/ory/hydra-client-go/v2"
"go.uber.org/zap"
"io"
"log" "log"
"net/http" "net/http"
"net/http/fcgi" "net/http/fcgi"
@@ -13,11 +18,48 @@ import (
"time" "time"
) )
var hydraConfig = hydra.NewConfiguration()
var (
hydraAdmin = flag.String("hydra-admin", "http://localhost:4445", "URL at which the Hydra admin API can be found")
hydraClient *hydra.APIClient
oauth2Logger *zap.Logger
)
func init() { func init() {
startup.Routes.Add(func(router *httprouter.Router) { startup.Routes.Add(func(router *httprouter.Router) {
router.Handler("GET", "/login", LoginStateMw(http.HandlerFunc(loginPage)))
router.Handler("GET", "/consent", LoginStateMw(http.HandlerFunc(consentPage)))
router.HandlerFunc("POST", "/login/krb5", loginKrb) router.HandlerFunc("POST", "/login/krb5", loginKrb)
router.HandlerFunc("POST", "/login/password", loginPassword) router.HandlerFunc("POST", "/login/password", loginPassword)
}) })
startup.Logger.Add(func() {
oauth2Logger = zap.L().Named("oauth2")
})
startup.PostFlags.Add(func() {
hydraConfig.Servers = []hydra.ServerConfiguration{
{
URL: *hydraAdmin,
},
}
hydraClient = hydra.NewAPIClient(hydraConfig)
})
}
func loginPage(w http.ResponseWriter, r *http.Request) {
if finishOauth2(w, r, nil, true) {
return
}
f, err := resources.StaticFiles.Open("login/index.html")
if err != nil {
ReportError(w, err)
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
_, _ = io.Copy(w, f)
} }
func loginKrb(w http.ResponseWriter, r *http.Request) { func loginKrb(w http.ResponseWriter, r *http.Request) {
@@ -111,4 +153,124 @@ func finishLogin(w http.ResponseWriter, req *http.Request, entry *ldap.Entry, ex
_ = backend.SessionStore.PutSession(req.Context(), session, sessionCache) _ = backend.SessionStore.PutSession(req.Context(), session, sessionCache)
// TODO: set cookies, return success // TODO: set cookies, return success
cookie := http.Cookie{
Name: "IPASSO_SID",
Value: SessionID,
Domain: req.Host,
Path: "/",
HttpOnly: true,
Secure: true,
Expires: session.Expiration,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, &cookie)
//cookie.Domain = "." + *domain
//http.SetCookie(w, &cookie)
// Handle OAuth2 if necessary
finishOauth2(w, req, &session, false)
}
// Finish OAuth2 processing if appropriate. Returns false if normal processing should continue
func finishOauth2(w http.ResponseWriter, req *http.Request, session *backend.Session, redirect bool) bool {
if session == nil {
beSession := GetSession(req.Context(), false)
if beSession == nil || beSession.Session == nil {
oauth2Logger.Debug("Not finishing oauth flow: no active session")
return false
}
session = beSession.Session
}
query := req.URL.Query()
if query.Has("login_challenge") {
challenge := query.Get("login_challenge")
acceptOAuth2LoginRequest := hydra.AcceptOAuth2LoginRequest{
Remember: hydra.PtrBool(true),
RememberFor: hydra.PtrInt64(int64(session.Expiration.Sub(time.Now()).Seconds())),
Subject: session.UserID,
}
oauth2Logger.Info("Accepting challenge", zap.Any("response", acceptOAuth2LoginRequest))
redirectTo, _, err := hydraClient.OAuth2Api.AcceptOAuth2LoginRequest(req.Context()).LoginChallenge(challenge).AcceptOAuth2LoginRequest(acceptOAuth2LoginRequest).Execute()
if err != nil {
oauth2Logger.Warn("AcceptOAuth2LoginRequest failed", zap.Error(err))
return false
}
if redirect {
http.Redirect(w, req, redirectTo.RedirectTo, http.StatusSeeOther)
} else {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(200)
_, _ = w.Write([]byte(redirectTo.RedirectTo))
}
oauth2Logger.Debug("Finishing oauth flow")
return true
}
oauth2Logger.Debug("Not finishing oauth flow: no challenge")
return false
}
type consentParams struct {
ConsentChallenge string
ConsentRequest string
}
type IdToken struct {
Name string `json:"name"`
Sub string `json:"sub"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
PreferredUsername string `json:"preferred_username"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
func consentPage(w http.ResponseWriter, req *http.Request) {
session := GetSession(req.Context(), true)
// We can assume that we've got a session, I think.
query := req.URL.Query()
consentChallenge := query.Get("consent_challenge")
ocr, _, err := hydraClient.OAuth2Api.GetOAuth2ConsentRequest(req.Context()).ConsentChallenge(consentChallenge).Execute()
if err != nil {
ReportError(w, err)
return
}
//reqJSON, _ := json.MarshalIndent(oAuth2ConsentRequest, "", " ")
//params := consentParams{
// ConsentRequest: string(reqJSON),
// ConsentChallenge: consentChallenge,
//}
//RenderPage(w, req, "consent", params)
// TODO: We should really have a consent page here, but in the mean time, not having it is OK
acr := hydra.AcceptOAuth2ConsentRequest{}
acr.Session = &hydra.AcceptOAuth2ConsentRequestSession{
AccessToken: map[string]string{},
IdToken: IdToken{
Name: session.Cache.DisplayName,
Sub: session.Session.UserID,
GivenName: session.Cache.GivenName,
FamilyName: session.Cache.FamilyName,
PreferredUsername: session.Session.UserID,
Email: session.Cache.Email,
EmailVerified: true,
},
}
acr.GrantScope = ocr.GetRequestedScope()
acr.GrantAccessTokenAudience = ocr.RequestedAccessTokenAudience
oauth2Logger.Info("Accepting consent", zap.Any("acr", acr))
redirectTo, _, err := hydraClient.OAuth2Api.AcceptOAuth2ConsentRequest(req.Context()).ConsentChallenge(consentChallenge).AcceptOAuth2ConsentRequest(acr).Execute()
if err != nil {
ReportError(w, err)
return
}
http.Redirect(w, req, redirectTo.RedirectTo, http.StatusSeeOther)
} }

63
app/ipasso/loginState.go Normal file
View File

@@ -0,0 +1,63 @@
package main
import (
"git.thequux.com/thequux/ipasso/util/startup"
"github.com/julienschmidt/httprouter"
"net/http"
"time"
)
func init() {
startup.Routes.Add(func(router *httprouter.Router) {
router.Handler("GET", "/login/info", LoginStateMw(http.HandlerFunc(stateServlet)))
})
}
type LoginState string
var (
LS_Valid LoginState = "VALID"
LS_LoggedOut LoginState = "EXPLICIT_LOGOUT"
LS_Unknown LoginState = "UNKNOWN"
LS_Invalid LoginState = "INVALID"
)
type PublicLoginState struct {
State LoginState `json:"login_state"`
Expiration *time.Time `json:"expiration,omitempty"`
UserId string `json:"uid,omitempty"`
LdapDn string `json:"ldap_dn,omitempty"`
Groups []string `json:"groups,omitempty"`
DisplayName string `json:"display_name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
Email string `json:"email,omitempty"`
}
func GetPublicState(s *Session) PublicLoginState {
ret := PublicLoginState{
State: s.State,
}
if s.Session != nil {
ret.Expiration = &s.Session.Expiration
ret.UserId = s.Session.UserID
ret.LdapDn = s.Session.LdapDN
}
if s.Cache != nil {
ret.Groups = s.Cache.Groups
ret.DisplayName = s.Cache.DisplayName
ret.GivenName = s.Cache.GivenName
ret.FamilyName = s.Cache.FamilyName
ret.Email = s.Cache.Email
}
return ret
}
func stateServlet(w http.ResponseWriter, req *http.Request) {
session := GetSession(req.Context(), true)
data := GetPublicState(session)
RenderPage(w, req, "status", data)
}

View File

@@ -45,7 +45,7 @@ func ReportError(w http.ResponseWriter, err error) {
var ctypes = [resources.ATComboCount]contentnegotiation.Negotiator{ var ctypes = [resources.ATComboCount]contentnegotiation.Negotiator{
contentnegotiation.NewNegotiator("application/json"), contentnegotiation.NewNegotiator("application/json"),
contentnegotiation.NewNegotiator("text/html", "application/json", "text/html"), contentnegotiation.NewNegotiator("text/html", "application/json"),
contentnegotiation.NewNegotiator("text/plain", "application/json"), contentnegotiation.NewNegotiator("text/plain", "application/json"),
contentnegotiation.NewNegotiator("text/html", "text/plain", "application/json"), contentnegotiation.NewNegotiator("text/html", "text/plain", "application/json"),
} }
@@ -57,6 +57,7 @@ func RenderPage(w http.ResponseWriter, req *http.Request, template string, data
negotiator := ctypes[act] negotiator := ctypes[act]
ctype, _, err := negotiator.Negotiate(req.Header.Get("Accept")) ctype, _, err := negotiator.Negotiate(req.Header.Get("Accept"))
if err != nil { if err != nil {
templateLogger.Warn("Negotiate failed", zap.Error(err), zap.String("ctype", ctype.String()))
w.Header().Set("Content-Length", "0") w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusUnsupportedMediaType) w.WriteHeader(http.StatusUnsupportedMediaType)
return return

View File

@@ -35,7 +35,7 @@ type SessionCache struct {
Groups []string Groups []string
DisplayName string DisplayName string
GivenName string GivenName string
SurName string FamilyName string
Email string Email string
} }

View File

@@ -3,6 +3,7 @@ package redis
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"git.thequux.com/thequux/ipasso/backend" "git.thequux.com/thequux/ipasso/backend"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"net/url" "net/url"
@@ -59,12 +60,12 @@ type RedisBackend struct {
var putSessionScript = redis.NewScript(` var putSessionScript = redis.NewScript(`
local skey = KEYS[1] local skey = KEYS[1]
local ckey = KEYS[2] local ckey = KEYS[2]
local session = ARGS[1] local session = ARGV[1]
local sexp = ARGS[2] local sexp = ARGV[2]
local cache = ARGS[3] local cache = ARGV[3]
local clifetime = ARGS[4] local clifetime = ARGV[4]
if redis.call("GET", skey) != "" then if redis.call("GET", skey) ~= "" then
return false return false
else else
redis.call("SET", skey, session, "EXAT", sexp) redis.call("SET", skey, session, "EXAT", sexp)
@@ -103,29 +104,32 @@ func (r *RedisBackend) PutSession(ctx context.Context, session backend.Session,
func (r *RedisBackend) GetSession(ctx context.Context, id string) (backend.Session, *backend.SessionCache, error) { func (r *RedisBackend) GetSession(ctx context.Context, id string) (backend.Session, *backend.SessionCache, error) {
var session backend.Session var session backend.Session
var cache backend.SessionCache var cache backend.SessionCache
var cachep = &cache
result, err := r.rdb.MGet(ctx, sessionKey(id), scacheKey(id)).Result() result, err := r.rdb.MGet(ctx, sessionKey(id), scacheKey(id)).Result()
if err != nil { if err != nil {
return session, nil, err return session, nil, err
} }
v, ok := result[0].([]byte) fmt.Printf("Result: %#v\n", result)
v, ok := result[0].(string)
if !ok { if !ok {
return backend.Session{}, nil, backend.ErrBackendData return backend.Session{}, nil, backend.ErrBackendData
} }
if err = json.Unmarshal(v, &session); err != nil { if err = json.Unmarshal([]byte(v), &session); err != nil {
return backend.Session{}, nil, err return backend.Session{}, nil, err
} }
v, ok = result[1].([]byte) v, ok = result[1].(string)
if !ok { if ok {
return backend.Session{}, nil, backend.ErrBackendData if err = json.Unmarshal([]byte(v), &cache); err != nil {
} return backend.Session{}, nil, err
if err = json.Unmarshal(v, &cache); err != nil { }
return backend.Session{}, nil, err } else {
cachep = nil
} }
return session, &cache, nil return session, cachep, nil
} }
func (r *RedisBackend) EndSession(ctx context.Context, id string) { func (r *RedisBackend) EndSession(ctx context.Context, id string) {

View File

@@ -1,6 +1,7 @@
# Authorization flow # Authorization flow
Current authorization state is passwd around via a signed cookie containing authorization details in JSON. Current authorization state is passed around via an opaque session ID in a cookie.
This cookie is stored in duplicate: once on the SSO domain and once on the protected domain (for use by RPX's)
Most pages do not have any form of authorization, and simply depend on the cookie. Most pages do not have any form of authorization, and simply depend on the cookie.
However, in order to log in, more complex flow is necessary. However, in order to log in, more complex flow is necessary.
@@ -26,7 +27,7 @@ The user may also submit the form with blank username/password, in which case au
These steps are performed in sequence, each returning the same data as `/login/status`. These steps are performed in sequence, each returning the same data as `/login/status`.
The process completes when the state is `VALID` or `EXPLICIT_LOGOUT`. The process completes when the state is `VALID` or `EXPLICIT_LOGOUT`.
1. Fetch `/login/spnego` via XHR GET. 1. Fetch `/login/krb5` via XHR GET.
2. Fetch `/login/x509` via XHR GET. 2. Fetch `/login/x509` via XHR GET.
Upon successful login, a redirect is performed to the page in the hidden field. Upon successful login, a redirect is performed to the page in the hidden field.
@@ -36,23 +37,8 @@ The process completes when the state is `VALID` or `EXPLICIT_LOGOUT`.
In addition to the `/login` endpoint, there exist several other useful endpoints: In addition to the `/login` endpoint, there exist several other useful endpoints:
* `/logout`: Set cookie to indicate logged-out state * `/logout`: Set cookie to indicate logged-out state
* `/status`: Fetch a representation of user's info. * `/login/status`: Fetch a representation of user's info.
If `Accept` requests `application/json`, this is JSON. If `Accept` requests `application/json`, this is JSON.
Otherwise, this is an HTML page Otherwise, this is an HTML page
* `/sigkey`: Fetch the current signing key. This is an Ed25519 key.
* `/refresh`: Refresh the user data cookie given the refresh token.
# Two cookies
In the above description, the login token was described as a single cookie.
It is, in fact, two cookies:
* A short-lived (~seconds) user data cookie
* A longer-lived (~8 hour) refresh token, tied to an auth server
If the user data cookie expires, the refresh token can be fed to `/refresh` to update the user data cookie.
The purpose of the user data cookie is solely to reduce the load on the SSO server; the resources fetched by a page will
generally be fetched within a few seconds of the initial request.
It is the reverse proxy's responsibility to refresh the user data cookie and translate it into headers or FCGI environment variables for the backend server.
This task can be assisted by ipasso-rpxagentd (to be specified).
An OAuth interface is also available on **TODO**

5
go.mod
View File

@@ -22,6 +22,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.1 // indirect github.com/google/uuid v1.3.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect
@@ -29,10 +30,14 @@ require (
github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/ory/hydra-client-go/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/redis/go-redis/v9 v9.2.1 // indirect github.com/redis/go-redis/v9 v9.2.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.8.4 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.13.0 // indirect golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.15.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
) )

18
go.sum
View File

@@ -19,6 +19,11 @@ github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -42,6 +47,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/ory/hydra-client-go/v2 v2.1.1 h1:3JatU9uFbw5XhF3lgPCas1l1Kok2v5Mq1p26zZwGHNg=
github.com/ory/hydra-client-go/v2 v2.1.1/go.mod h1:IiIwChp/9wRvPoyFQblqPvg78uVishCCrV9+/M7Pl34=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -72,6 +79,7 @@ golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -82,6 +90,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -101,6 +111,7 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -112,6 +123,13 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -3,9 +3,10 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SSO Login</title> <title>SSO Login</title>
<script src="/login/login.js"></script>
</head> </head>
<body> <body>
<form action="password" method="post"> <form method="post">
<table> <table>
<tr> <tr>
<td><label for="user">User</label></td> <td><label for="user">User</label></td>
@@ -18,7 +19,7 @@
<tr> <tr>
<td></td> <td></td>
<td> <td>
<input type="submit" value="Log in"> <input type="submit" value="Log in" id="doit">
</td> </td>
</tr> </tr>
</table> </table>

View File

@@ -0,0 +1,33 @@
// Attempt to log in using ambient authority (e.g., Kerberos, SSL, etc)
async function tryLoginAmbient() {
let resp = await fetch("/login/krb5" + window.location.search, {method: "POST", redirect: "manual"})
if (resp.ok) {
window.location = await resp.text()
}
}
async function tryLoginPassword() {
let userField = document.getElementById("user")
let passwordField = document.getElementById("password")
let doit = document.getElementById("doit")
try {
userField.enabled = passwordField.enabled = doit.enabled = false
let resp = await fetch("/login/password" + window.location.search, {method: "POST"})
if (resp.ok) {
window.location = await resp.text()
}
} catch (e) {
userField.enabled = passwordField.enabled = doit.enabled = true
throw e
}
}
tryLoginAmbient() // don't bother awaiting, just let 'er go
document.addEventListener("load", () => {
document.getElementById("doit").form.addEventListener("submit", function (e) {
e.preventDefault()
tryLoginPassword()
return false
})
})

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Consent request</title>
</head>
<body>
<pre>{{ .ConsentRequest }}</pre>
<hr>
</body>
</html>

View File

@@ -0,0 +1 @@
{{ .ConsentRequest }}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login status</title>
</head>
<body>
{{-
</body>
</html>