first working version

This commit is contained in:
ston1th 2022-07-10 15:51:31 +02:00
commit c9dfecc519
21 changed files with 296 additions and 178 deletions

View file

@ -4,7 +4,6 @@ package server
import (
"context"
"encoding/json"
"errors"
"html/template"
"net/http"
@ -87,7 +86,7 @@ type loginData struct {
type consentData struct {
ConsentChallenge string
Info *consentInfo
Info *client.ConsentRequest
}
func (c *Context) Exec() {
@ -302,155 +301,117 @@ func (c *Context) Admin() bool {
return c.Token.Claims.GetBool(adminClaim)
}
type respBody struct {
URL string `json:"redirect_to,omitempty"`
Error string `json:"error,omitempty"`
}
func (c *Context) ApproveLogin(user, challenge string) (rdr string, err error) {
req := c.Srv.API.AdminApi.AcceptLoginRequest(context.Background())
req.AcceptLoginRequest(*client.NewAcceptLoginRequest(user))
req.LoginChallenge(challenge)
_, resp, err := req.Execute()
cr, resp, err := c.Srv.API.AdminApi.AcceptLoginRequest(context.Background()).
AcceptLoginRequest(*client.NewAcceptLoginRequest(user)).
LoginChallenge(challenge).
Execute()
if err != nil {
return
}
defer resp.Body.Close()
rb := new(respBody)
err = json.NewDecoder(resp.Body).Decode(rb)
if err != nil {
return
}
if rb.Error != "" {
err = errors.New(rb.Error)
return
}
rdr = rb.URL
rdr = cr.RedirectTo
return
}
func (c *Context) ApproveConsent(challenge string) (rdr string, err error) {
req := c.Srv.API.AdminApi.AcceptConsentRequest(context.Background()).
AcceptConsentRequest(*client.NewAcceptConsentRequest()).
ConsentChallenge(challenge)
_, resp, err := req.Execute()
ci, err := c.Consent(challenge)
if err != nil {
return
}
accept := client.NewAcceptConsentRequest()
accept.GrantScope = ci.RequestedScope
accept.GrantAccessTokenAudience = ci.RequestedAccessTokenAudience
accept.Remember = new(bool)
*accept.Remember = true
cr, resp, err := c.Srv.API.AdminApi.AcceptConsentRequest(context.Background()).
AcceptConsentRequest(*accept).
ConsentChallenge(challenge).
Execute()
if err != nil {
return
}
defer resp.Body.Close()
rb := new(respBody)
err = json.NewDecoder(resp.Body).Decode(rb)
if err != nil {
return
}
if rb.Error != "" {
err = errors.New(rb.Error)
return
}
rdr = rb.URL
rdr = cr.RedirectTo
return
}
func (c *Context) RejectConsent(challenge string) (rdr string, err error) {
req := c.Srv.API.AdminApi.RejectConsentRequest(context.Background()).
cr, resp, err := c.Srv.API.AdminApi.RejectConsentRequest(context.Background()).
RejectRequest(*client.NewRejectRequest()).
ConsentChallenge(challenge)
_, resp, err := req.Execute()
ConsentChallenge(challenge).
Execute()
if err != nil {
return
}
defer resp.Body.Close()
rb := new(respBody)
err = json.NewDecoder(resp.Body).Decode(rb)
if err != nil {
return
}
if rb.Error != "" {
err = errors.New(rb.Error)
return
}
rdr = rb.URL
rdr = cr.RedirectTo
return
}
func (c *Context) Consent(challenge string) (ci *consentInfo, err error) {
req := c.Srv.API.AdminApi.GetConsentRequest(context.Background()).
ConsentChallenge(challenge)
_, resp, err := req.Execute()
func (c *Context) Consent(challenge string) (cr *client.ConsentRequest, err error) {
cr, resp, err := c.Srv.API.AdminApi.GetConsentRequest(context.Background()).
ConsentChallenge(challenge).
Execute()
if err != nil {
return
}
defer resp.Body.Close()
ci = new(consentInfo)
err = json.NewDecoder(resp.Body).Decode(ci)
return
}
type sess struct {
ID string
Time string
}
func (c *Context) Sessions(user string) (m map[string]sess, err error) {
ss, resp, err := c.Srv.API.AdminApi.ListSubjectConsentSessions(context.Background()).
Subject(user).
Execute()
if err != nil {
return
}
if ci.Error != "" {
err = errors.New(ci.Error)
defer resp.Body.Close()
m = make(map[string]sess)
for _, s := range ss {
name := s.ConsentRequest.Client.ClientName
if name == nil {
continue
}
n := *name
cid := s.ConsentRequest.Client.ClientId
if cid == nil {
continue
}
id := *cid
if _, ok := m[n]; !ok {
m[n] = sess{
id,
s.HandledAt.Format(timeFmt),
}
}
}
return
}
type consentInfo struct {
Acr string `json:"acr"`
Amr []string `json:"amr"`
Challenge string `json:"challenge"`
Error string `json:"error,omitempty"`
Client struct {
AllowedCorsOrigins []string `json:"allowed_cors_origins"`
Audience []string `json:"audience"`
BackchannelLogoutSessionRequired bool `json:"backchannel_logout_session_required"`
BackchannelLogoutURI string `json:"backchannel_logout_uri"`
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
ClientSecret string `json:"client_secret"`
ClientSecretExpiresAt int `json:"client_secret_expires_at"`
ClientURI string `json:"client_uri"`
Contacts []string `json:"contacts"`
CreatedAt time.Time `json:"created_at"`
FrontchannelLogoutSessionRequired bool `json:"frontchannel_logout_session_required"`
FrontchannelLogoutURI string `json:"frontchannel_logout_uri"`
GrantTypes []string `json:"grant_types"`
Jwks struct {
} `json:"jwks"`
JwksURI string `json:"jwks_uri"`
LogoURI string `json:"logo_uri"`
Metadata struct {
} `json:"metadata"`
Owner string `json:"owner"`
PolicyURI string `json:"policy_uri"`
PostLogoutRedirectUris []string `json:"post_logout_redirect_uris"`
RedirectUris []string `json:"redirect_uris"`
RegistrationAccessToken string `json:"registration_access_token"`
RegistrationClientURI string `json:"registration_client_uri"`
RequestObjectSigningAlg string `json:"request_object_signing_alg"`
RequestUris []string `json:"request_uris"`
ResponseTypes []string `json:"response_types"`
Scope string `json:"scope"`
SectorIdentifierURI string `json:"sector_identifier_uri"`
SubjectType string `json:"subject_type"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
TosURI string `json:"tos_uri"`
UpdatedAt time.Time `json:"updated_at"`
UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg"`
} `json:"client"`
LoginChallenge string `json:"login_challenge"`
LoginSessionID string `json:"login_session_id"`
OidcContext struct {
AcrValues []string `json:"acr_values"`
Display string `json:"display"`
IDTokenHintClaims struct {
} `json:"id_token_hint_claims"`
LoginHint string `json:"login_hint"`
UILocales []string `json:"ui_locales"`
} `json:"oidc_context"`
RequestURL string `json:"request_url"`
RequestedAccessTokenAudience []string `json:"requested_access_token_audience"`
RequestedScope []string `json:"requested_scope"`
Skip bool `json:"skip"`
Subject string `json:"subject"`
func (c *Context) RevokeSession(user, client string, all bool) (err error) {
if client == "" && !all {
return
}
req := c.Srv.API.AdminApi.RevokeConsentSessions(context.Background()).
Subject(user)
if all {
req = req.All(all)
} else {
req = req.Client(client)
}
resp, err := req.Execute()
if err != nil {
return
}
defer resp.Body.Close()
return
}
type ctxHandler func(*Context)

View file

@ -106,8 +106,8 @@ func indexHandler(ctx *Context) {
ctx.Redirect(login, http.StatusFound)
}
func profileHandler(ctx *Context) {
ctx.Redirect(uroot+"edit/"+ctx.User(), http.StatusFound)
func sessionsHandler(ctx *Context) {
ctx.Redirect(uroot+"sessions/"+ctx.User(), http.StatusFound)
}
func loginHandler(ctx *Context) {
@ -123,7 +123,7 @@ func loginHandler(ctx *Context) {
BodyTitle: "Login",
}
if ctx.LoggedOn() {
rdr := profile
rdr := sessions
if data.LoginChallenge != "" {
rdr, err = ctx.ApproveLogin(ctx.User(), data.LoginChallenge)
if err != nil {
@ -172,7 +172,7 @@ func loginHandler(ctx *Context) {
}
log.Info("user logged in", "user", data.User)
ctx.Login(u, remember)
rdr := profile
rdr := sessions
if data.LoginChallenge != "" {
rdr, err = ctx.ApproveLogin(data.User, data.LoginChallenge)
if err != nil {
@ -187,7 +187,7 @@ func loginHandler(ctx *Context) {
func loginTotpHandler(ctx *Context) {
if ctx.LoggedOn() {
ctx.Redirect(profile, http.StatusFound)
ctx.Redirect(sessions, http.StatusFound)
return
}
ctx.Template("loginTotpHandler")
@ -222,7 +222,7 @@ func loginTotpHandler(ctx *Context) {
}
log.Info("totp successful")
ctx.Login(u, ctx.Remember())
rdr := root
rdr := sessions
if data.LoginChallenge != "" {
rdr, err = ctx.ApproveLogin(user, data.LoginChallenge)
if err != nil {
@ -239,21 +239,30 @@ func loginTotpHandler(ctx *Context) {
func consentHandler(ctx *Context) {
ctx.Template("consentHandler")
ctx.Data = webData{
Title: "Consent",
BodyTitle: "Consent",
Title: "Authorize",
BodyTitle: "Authorize",
}
var data consentData
switch ctx.Method() {
case "GET":
data.ConsentChallenge = ctx.Query("consent_challenge")
ctx.Data.Data = data
ci, err := ctx.Consent(data.ConsentChallenge)
cr, err := ctx.Consent(data.ConsentChallenge)
if err != nil {
ctx.log.Error(err, "error getting consent info")
ctx.Error("error getting consent info")
return
}
data.Info = ci
if cr.GetSkip() {
rdr, err := ctx.ApproveConsent(data.ConsentChallenge)
if err != nil {
ctx.log.Error(err, "error approving consent")
ctx.Error("error approving consent")
return
}
ctx.Redirect(rdr, http.StatusFound)
}
data.Info = cr
ctx.Data.Data = data
ctx.Exec()
case "POST":
@ -321,11 +330,8 @@ func userNewHandler(ctx *Context) {
email := ctx.Form("email")
password := ctx.Form("password")
repeat := ctx.Form("repeat")
admin := ctx.Form("admin")
adm := false
if admin == "0" {
adm = true
}
admin := ctx.Form("admin") == "0"
disabled := ctx.Form("disabled") == "0"
if user == "" || email == "" || password == "" {
ctx.Error("empty user, email or password")
@ -335,11 +341,11 @@ func userNewHandler(ctx *Context) {
ctx.Error("passwords do not match")
return
}
if err := ctx.Srv.DB.CreateUser(user, email, password, adm); err != nil {
if err := ctx.Srv.DB.CreateUser(user, email, password, admin, disabled); err != nil {
ctx.Error(err)
return
}
ctx.log.Info("user created", "user", user, "actor", ctx.User())
ctx.log.Info("user created", "user", user, "actor", ctx.User(), "admin", admin, "disabled", disabled)
ctx.Redirect(users, http.StatusFound)
}
}
@ -364,25 +370,27 @@ func userEditHandler(ctx *Context) {
return
}
user := ctx.Var("user")
log := ctx.log.WithValues("user", user, "actor", ctx.User())
self := ctx.User()
log := ctx.log.WithValues("user", user, "actor", self)
password := ctx.Form("password")
repeat := ctx.Form("repeat")
admin := ctx.Form("admin")
adm := false
if admin == "0" {
adm = true
}
admin := ctx.Form("admin") == "0"
disabled := ctx.Form("disabled") == "0"
if password != repeat {
ctx.Error("passwords do not match")
return
}
if ctx.Admin() {
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
if err := ctx.Srv.DB.AdminUpdateUser(user, password, admin, disabled); err != nil {
ctx.Error(err)
return
}
log.Info("user updated", "admin", adm)
log.Info("user updated", "admin", admin, "disabled", disabled)
if user == self {
logoutHandler(ctx)
return
}
ctx.Redirect(users, http.StatusFound)
return
}
@ -461,6 +469,40 @@ func userTotpHandler(ctx *Context) {
}
}
func userSessionsHandler(ctx *Context) {
ctx.Template("userSessionsHandler")
ctx.Data = webData{
Title: "Sessions",
BodyTitle: "Sessions",
}
user := ctx.Var("user")
log := ctx.log.WithValues("user", user)
switch ctx.Method() {
case "GET":
s, err := ctx.Sessions(user)
if err != nil {
log.Error(err, "error getting sessions")
ctx.Error("error getting sessions")
return
}
ctx.Data.Data = s
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
client := ctx.Form("client")
all := ctx.Form("all") == "all"
err := ctx.RevokeSession(user, client, all)
if err != nil {
log.Error(err, "error revoking session", "client", client, "all", all)
ctx.Error("error revoking session")
return
}
sessionsHandler(ctx)
}
}
func userUnlockHandler(ctx *Context) {
user := ctx.Var("user")
log := ctx.log.WithValues("user", user, "actor", ctx.User())
@ -502,7 +544,7 @@ func userDelHandler(ctx *Context) {
log.Info("delete successful")
}
if user == self {
ctx.Redirect(logout, http.StatusFound)
logoutHandler(ctx)
return
}
ctx.Redirect(users, http.StatusFound)

View file

@ -10,4 +10,6 @@ const (
day = time.Hour * 24
min = time.Second * 30
max = day * 90
timeFmt = "2006-01-02 15:04:05"
)

View file

@ -9,20 +9,21 @@ type route struct {
}
const (
root = "/"
login = root + "login"
totp = root + "totp"
profile = root + "profile"
consent = root + "consent"
users = root + "users"
logout = root + "logout"
uroot = root + "user/"
root = "/"
login = root + "login"
totp = root + "totp"
sessions = root + "sessions"
consent = root + "consent"
users = root + "users"
logout = root + "logout"
uroot = root + "user/"
userNew = uroot + "new"
userEdit = uroot + "edit/{user:[a-zA-Z0-9]+$}"
userDel = uroot + "del/{user:[a-zA-Z0-9]+$}"
userTotp = uroot + "totp/{user:[a-zA-Z0-9]+$}"
userUnlock = uroot + "unlock/{user:[a-zA-Z0-9]+$}"
userNew = uroot + "new"
userEdit = uroot + "edit/{user:[a-zA-Z0-9]+$}"
userDel = uroot + "del/{user:[a-zA-Z0-9]+$}"
userTotp = uroot + "totp/{user:[a-zA-Z0-9]+$}"
userSessions = uroot + "sessions/{user:[a-zA-Z0-9]+$}"
userUnlock = uroot + "unlock/{user:[a-zA-Z0-9]+$}"
)
var routes = []route{
@ -48,15 +49,15 @@ var routes = []route{
{
consent,
jwtHandler(
userAuthHandler(
authHandler(
consentHandler)),
[]string{"GET", "POST"},
},
{
profile,
sessions,
jwtHandler(
userAuthHandler(
profileHandler)),
authHandler(
sessionsHandler)),
[]string{"GET"},
},
{
@ -100,6 +101,13 @@ var routes = []route{
userTotpHandler)),
[]string{"GET", "POST"},
},
{
userSessions,
jwtHandler(
userAuthHandler(
userSessionsHandler)),
[]string{"GET", "POST"},
},
{
userUnlock,
jwtHandler(

View file

@ -43,6 +43,7 @@ func (s *Server) loadTemplates() (err error) {
// pages
s.ts.Parse(tfs, "notFoundHandler", "index.html", "menu.html", "notFound.html")
s.ts.Parse(tfs, "forbiddenHandler", "index.html", "menu.html", "forbidden.html")
s.ts.Parse(tfs, "loginHandler", "index.html", "menu.html", "login.html")
s.ts.Parse(tfs, "loginTotpHandler", "index.html", "menu.html", "loginTotp.html")
s.ts.Parse(tfs, "consentHandler", "index.html", "menu.html", "consent.html")
@ -51,6 +52,7 @@ func (s *Server) loadTemplates() (err error) {
s.ts.Parse(tfs, "userNewHandler", "index.html", "menu.html", "userNew.html")
s.ts.Parse(tfs, "userEditHandler", "index.html", "menu.html", "userEdit.html")
s.ts.Parse(tfs, "userDelHandler", "index.html", "menu.html", "userDel.html")
s.ts.Parse(tfs, "userSessionsHandler", "index.html", "menu.html", "userSessions.html")
// totp
s.ts.Parse(tfs, "userTotpHandler", "index.html", "menu.html", "userTotp.html")

View file

@ -1,15 +1,19 @@
{{define "body"}}
{{if .Data}}
<div class="row">
<div class="col-xs-4 offset-4">
<div class="col-xs-4 offset-3">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
<strong>{{.BodyTitle}} - {{.Data.Info.Client.ClientName}}</strong>
</div>
<div class="card-body">
<pre>
{{.Data.Info}}
</pre>
The client <code>{{.Data.Info.Client.ClientName}}</code> is requesting access to your data.<br>
Requested Scopes:
<ul>
{{range $item := .Data.Info.RequestedScope}}
<li>{{$item}}</li>
{{end}}
</ul>
<form class="form-horizontal" action="/consent" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<input type="hidden" name="consent_challenge" value="{{.Data.ConsentChallenge}}">

View file

@ -0,0 +1,17 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-danger mx-auto">
<div class="card-header">
<strong>403 - Forbidden</strong>
</div>
<div class="card-body">
<h5>Access to this page is restricted</h5>
<a href="/" class="btn btn-sm btn-primary">Back</a>
</div>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -9,7 +9,9 @@
<div class="card-body">
<form class="form-horizontal" action="/login" method="post">
<input type="hidden" name="token" value="{{.Token}}">
{{if .Data}}
<input type="hidden" name="login_challenge" value="{{.Data.LoginChallenge}}">
{{end}}
<div class="form-group">
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.User}}" autofocus required>

View file

@ -9,7 +9,9 @@
<div class="card-body">
<form class="form-horizontal" action="/totp" method="post">
<input type="hidden" name="token" value="{{.Token}}">
{{if .Data}}
<input type="hidden" name="login_challenge" value="{{.Data.LoginChallenge}}">
{{end}}
<div class="form-group">
<label class="col-form-label" for="pin">PIN</label>
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>

View file

@ -1,11 +1,13 @@
{{define "menu"}}
{{if .Admin}}
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
</ul>
{{end}}
<ul class="nav navbar-nav ml-auto">
{{if .Login}}
{{if .Admin}}
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
{{else}}
<li class="nav-item"><a class="nav-link" href="/user/sessions/{{.User}}">Sessions</a></li>
<li class="nav-item"><a class="nav-link" href="/user/edit/{{.User}}">Profile</a></li>
{{end}}
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
{{else}}
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>

View file

@ -8,7 +8,7 @@
<strong>Delete {{.Data.Username}}?</strong>
</div>
<div class="card-body">
<p>You are about to delete user <a href="/{{.Data.Username}}">{{.Data.Username}}</a>.</p>
<p>You are about to delete user <a href="/user/edit/{{.Data.Username}}">{{.Data.Username}}</a>.</p>
<form class="form-horizontal" action="/user/del/{{.Data.Username}}" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>

View file

@ -41,6 +41,16 @@
<label class="custom-control-label" for="admin">Admin Privileges</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
{{if .Data.Disabled}}
<input type="checkbox" class="custom-control-input" id="disabled" name="disabled" value="0" checked>
{{else}}
<input type="checkbox" class="custom-control-input" id="disabled" name="disabled" value="0">
{{end}}
<label class="custom-control-label" for="disabled">Disabled</label>
</div>
</div>
{{end}}
<button class="btn btn-sm btn-primary" type="submit">Update</button>
{{if .Admin}}

View file

@ -30,6 +30,12 @@
<label class="custom-control-label" for="admin">Admin Privileges</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="disabled" name="disabled" value="0">
<label class="custom-control-label" for="disabled">Disabled</label>
</div>
</div>
<button class="btn btn-sm btn-primary" type="submit">Create</button>
<a href="/users" class="btn btn-sm btn-primary">Back</a>
</form>

View file

@ -0,0 +1,50 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
{{$user := .User}}
{{$token := .Token}}
{{if .Data}}
<div class="row mb-3">
<div class="col-xs-5">
<form class="form-horizontal" action="/user/sessions/{{$user}}" method="post">
<input type="hidden" name="token" value="{{$token}}">
<input type="hidden" name="all" value="all">
<button class="btn btn-sm btn-danger" type="submit">Revoke All</button>
</form>
</div>
</div>
{{end}}
<div class="row">
<div class="col">
{{if .Data}}
<table class="table table-hover">
<thead><tr>
<th scope="col">Client Name</th>
<th scope="col">Created</th>
<th scope="col">Options</th>
</tr></thead>
<tbody>
{{range $name, $s := .Data}}
<tr>
<th scope="row">{{$name}}</th>
<td>{{$s.Time}}</td>
<td>
<form class="form-horizontal" action="/user/sessions/{{$user}}" method="post">
<input type="hidden" name="token" value="{{$token}}">
<input type="hidden" name="client" value="{{$s.ID}}">
<button class="btn btn-sm btn-warning" type="submit">Revoke</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
No active sessions found.
{{end}}
</div>
</div>
{{end}}

View file

@ -31,13 +31,15 @@
<td>{{$item.Username}}</td>
<td>{{$item.Email}}</td>
<td>
{{if $item.Admin}}<span class="badge badge-danger">admin</span>{{end}}
{{if $item.Secret}}<span class="badge badge-success">totp</span>{{end}}
{{if eq $item.Locked 3}}<span class="badge badge-warning">locked</span>{{end}}
{{if $item.Admin}}<span class="badge badge-danger">admin</span>{{end}}
{{if $item.Disabled}}<span class="badge badge-warning">disabled</span>{{end}}
</td>
<td>
<div class="btn-group">
{{if $admin}}
<a class="btn btn-sm btn-primary" href="/user/sessions/{{$item.Username}}">Sessions</a>
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
<a class="btn btn-sm btn-danger" href="/user/del/{{$item.Username}}">Delete</a>
{{else}}