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 {
cache.GivenName = strings.Join(attr.Values, " ")
} 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 {
cache.Email = attr.Values[0]
} else if attr.Name == "memberOf" {

View File

@@ -2,10 +2,15 @@ package main
import (
"errors"
"flag"
"git.thequux.com/thequux/ipasso/backend"
"git.thequux.com/thequux/ipasso/resources"
"git.thequux.com/thequux/ipasso/util/startup"
"github.com/go-ldap/ldap/v3"
"github.com/julienschmidt/httprouter"
hydra "github.com/ory/hydra-client-go/v2"
"go.uber.org/zap"
"io"
"log"
"net/http"
"net/http/fcgi"
@@ -13,11 +18,48 @@ import (
"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() {
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/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) {
@@ -111,4 +153,124 @@ func finishLogin(w http.ResponseWriter, req *http.Request, entry *ldap.Entry, ex
_ = backend.SessionStore.PutSession(req.Context(), session, sessionCache)
// 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{
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/html", "text/plain", "application/json"),
}
@@ -57,6 +57,7 @@ func RenderPage(w http.ResponseWriter, req *http.Request, template string, data
negotiator := ctypes[act]
ctype, _, err := negotiator.Negotiate(req.Header.Get("Accept"))
if err != nil {
templateLogger.Warn("Negotiate failed", zap.Error(err), zap.String("ctype", ctype.String()))
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusUnsupportedMediaType)
return

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
# 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.
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`.
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.
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:
* `/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.
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/hashicorp/go-uuid v1.0.3 // 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/goidentity/v6 v6.0.1 // 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/redis/go-redis/v9 v9.2.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.13.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-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/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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
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/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/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-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
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-20220722155255-886fb9371eb4/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.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.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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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.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-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/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=

View File

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