Commit fada009f authored by Sietse Ringers's avatar Sietse Ringers
Browse files

feat: support static QRs in irma server and irmaclient

parent 33cdda49
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"testing" "testing"
"time" "time"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test" "github.com/privacybydesign/irmago/internal/test"
"github.com/privacybydesign/irmago/server" "github.com/privacybydesign/irmago/server"
"github.com/privacybydesign/irmago/server/irmaserver" "github.com/privacybydesign/irmago/server/irmaserver"
...@@ -118,5 +119,18 @@ var JwtServerConfiguration = &requestorserver.Configuration{ ...@@ -118,5 +119,18 @@ var JwtServerConfiguration = &requestorserver.Configuration{
AuthenticationKey: "eGE2PSomOT84amVVdTU+LmYtJXJWZ2BmNjNwSGltCg==", AuthenticationKey: "eGE2PSomOT84amVVdTU+LmYtJXJWZ2BmNjNwSGltCg==",
}, },
}, },
StaticSessions: map[string]interface{}{
"staticsession": irma.ServiceProviderRequest{
RequestorBaseRequest: irma.RequestorBaseRequest{
CallbackUrl: "http://localhost:48685",
},
Request: &irma.DisclosureRequest{
BaseRequest: irma.BaseRequest{LDContext: irma.LDContextDisclosureRequest},
Disclose: irma.AttributeConDisCon{
{{irma.NewAttributeRequest("irma-demo.RU.studentCard.level")}},
},
},
},
},
JwtPrivateKeyFile: filepath.Join(testdata, "jwtkeys", "sk.pem"), JwtPrivateKeyFile: filepath.Join(testdata, "jwtkeys", "sk.pem"),
} }
package sessiontest package sessiontest
import ( import (
"context"
"encoding/json" "encoding/json"
"net/http"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
"path/filepath" "time"
"github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs" "github.com/privacybydesign/irmago/internal/fs"
...@@ -388,3 +391,41 @@ func TestDownloadSchemeManager(t *testing.T) { ...@@ -388,3 +391,41 @@ func TestDownloadSchemeManager(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, exists) require.True(t, exists)
} }
func TestStaticQRSession(t *testing.T) {
client, _ := parseStorage(t)
defer test.ClearTestStorage(t)
StartRequestorServer(JwtServerConfiguration)
defer StopRequestorServer()
// start server to receive session result callback after the session
var received bool
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
received = true
})
s := &http.Server{Addr: ":48685", Handler: mux}
go func() { _ = s.ListenAndServe() }()
// setup static QR and other variables
qr := &irma.Qr{
Type: irma.ActionRedirect,
URL: "http://localhost:48682/session/-/static/staticsession",
}
bts, err := json.Marshal(qr)
require.NoError(t, err)
localhost := "localhost"
host := irma.NewTranslatedString(&localhost)
c := make(chan *SessionResult)
// Perform session
client.NewSession(string(bts), TestHandler{t, c, client, host})
if result := <-c; result != nil {
require.NoError(t, result.Err)
}
// give irma server time to post session result to the server started above, and check the call was received
time.Sleep(200 * time.Millisecond)
require.NoError(t, s.Shutdown(context.Background()))
require.True(t, received)
}
...@@ -164,6 +164,19 @@ func (client *Client) newSchemeSession(qr *irma.SchemeManagerRequest, handler Ha ...@@ -164,6 +164,19 @@ func (client *Client) newSchemeSession(qr *irma.SchemeManagerRequest, handler Ha
// newQrSession creates and starts a new interactive IRMA session // newQrSession creates and starts a new interactive IRMA session
func (client *Client) newQrSession(qr *irma.Qr, handler Handler) SessionDismisser { func (client *Client) newQrSession(qr *irma.Qr, handler Handler) SessionDismisser {
if qr.Type == irma.ActionRedirect {
newqr := &irma.Qr{}
if err := irma.NewHTTPTransport("").Post(qr.URL, newqr, struct{}{}); err != nil {
handler.Failure(&irma.SessionError{ErrorType: irma.ErrorTransport, Err: errors.Wrap(err, 0)})
return nil
}
if newqr.Type == irma.ActionRedirect { // explicitly avoid infinite recursion
handler.Failure(&irma.SessionError{ErrorType: irma.ErrorInvalidRequest, Err: errors.New("infinite static QR recursion")})
return nil
}
return client.newQrSession(newqr, handler)
}
u, _ := url.ParseRequestURI(qr.URL) // Qr validator already checked this for errors u, _ := url.ParseRequestURI(qr.URL) // Qr validator already checked this for errors
session := &session{ session := &session{
ServerURL: qr.URL, ServerURL: qr.URL,
......
...@@ -163,6 +163,7 @@ const ( ...@@ -163,6 +163,7 @@ const (
ActionDisclosing = Action("disclosing") ActionDisclosing = Action("disclosing")
ActionSigning = Action("signing") ActionSigning = Action("signing")
ActionIssuing = Action("issuing") ActionIssuing = Action("issuing")
ActionRedirect = Action("redirect")
ActionUnknown = Action("unknown") ActionUnknown = Action("unknown")
) )
...@@ -312,6 +313,7 @@ func (qr *Qr) Validate() (err error) { ...@@ -312,6 +313,7 @@ func (qr *Qr) Validate() (err error) {
case ActionDisclosing: // nop case ActionDisclosing: // nop
case ActionIssuing: // nop case ActionIssuing: // nop
case ActionSigning: // nop case ActionSigning: // nop
case ActionRedirect: // nop
default: default:
return errors.New("Unsupported session type") return errors.New("Unsupported session type")
} }
......
...@@ -125,6 +125,7 @@ func setFlags(cmd *cobra.Command, production bool) error { ...@@ -125,6 +125,7 @@ func setFlags(cmd *cobra.Command, production bool) error {
issHelp += " (default *)" issHelp += " (default *)"
} }
flags.StringSlice("issue-perms", nil, issHelp) flags.StringSlice("issue-perms", nil, issHelp)
flags.String("static-sessions", "", "preconfigured static sessions (in JSON)")
flags.Lookup("no-auth").Header = `Requestor authentication and default requestor permissions` flags.Lookup("no-auth").Header = `Requestor authentication and default requestor permissions`
flags.StringP("jwt-issuer", "j", "irmaserver", "JWT issuer") flags.StringP("jwt-issuer", "j", "irmaserver", "JWT issuer")
...@@ -274,11 +275,32 @@ func configure(cmd *cobra.Command) error { ...@@ -274,11 +275,32 @@ func configure(cmd *cobra.Command) error {
} }
} }
if err = handleMapOrString("static-sessions", &conf.StaticSessions); err != nil {
return err
}
logger.Debug("Done configuring") logger.Debug("Done configuring")
return nil return nil
} }
func handleMapOrString(key string, dest interface{}) error {
var m map[string]interface{}
var err error
if val, flagOrEnv := viper.Get(key).(string); !flagOrEnv || val != "" {
if m, err = cast.ToStringMapE(viper.Get(key)); err != nil {
return errors.WrapPrefix(err, "Failed to unmarshal "+key+" from flag or env var", 0)
}
}
if len(m) == 0 {
return nil
}
if err := mapstructure.Decode(m, dest); err != nil {
return errors.WrapPrefix(err, "Failed to unmarshal "+key+" from config file", 0)
}
return nil
}
func handlePermission(typ string) []string { func handlePermission(typ string) []string {
if !viper.IsSet(typ) && (!viper.GetBool("production") || typ != "issue-perms") { if !viper.IsSet(typ) && (!viper.GetBool("production") || typ != "issue-perms") {
return []string{"*"} return []string{"*"}
......
...@@ -3,6 +3,7 @@ package requestorserver ...@@ -3,6 +3,7 @@ package requestorserver
import ( import (
"crypto/rsa" "crypto/rsa"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"regexp" "regexp"
"strconv" "strconv"
...@@ -65,7 +66,10 @@ type Configuration struct { ...@@ -65,7 +66,10 @@ type Configuration struct {
// Host static files under this URL prefix // Host static files under this URL prefix
StaticPrefix string `json:"static_prefix" mapstructure:"static_prefix"` StaticPrefix string `json:"static_prefix" mapstructure:"static_prefix"`
jwtPrivateKey *rsa.PrivateKey StaticSessions map[string]interface{} `json:"static_sessions"`
staticSessions map[string]irma.RequestorRequest
jwtPrivateKey *rsa.PrivateKey
} }
// Permissions specify which attributes or credential a requestor may verify or issue. // Permissions specify which attributes or credential a requestor may verify or issue.
...@@ -247,6 +251,32 @@ func (conf *Configuration) initialize() error { ...@@ -247,6 +251,32 @@ func (conf *Configuration) initialize() error {
} }
} }
if len(conf.StaticSessions) != 0 && conf.jwtPrivateKey == nil {
conf.Logger.Warn("Static sessions enabled and no JWT private key installed. Ensure that POSTs to the callback URLs of static sessions are trustworthy by keeping the callback URLs secret and by using HTTPS.")
}
conf.staticSessions = make(map[string]irma.RequestorRequest)
for name, r := range conf.StaticSessions {
if !regexp.MustCompile("^[a-zA-Z0-9_]+$").MatchString(name) {
return errors.Errorf("static session name %s not allowed, must be alphanumeric", name)
}
j, err := json.Marshal(r)
if err != nil {
return errors.WrapPrefix(err, "failed to parse static session request "+name, 0)
}
rrequest, err := server.ParseSessionRequest(j)
if err != nil {
return errors.WrapPrefix(err, "failed to parse static session request "+name, 0)
}
action := rrequest.SessionRequest().Action()
if action != irma.ActionDisclosing && action != irma.ActionSigning {
return errors.Errorf("static session %s must be either a disclosing or signing session", name)
}
if rrequest.Base().CallbackUrl == "" {
return errors.Errorf("static session %s has no callback URL", name)
}
conf.staticSessions[name] = rrequest
}
return nil return nil
} }
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
...@@ -163,13 +164,21 @@ var corsOptions = cors.Options{ ...@@ -163,13 +164,21 @@ var corsOptions = cors.Options{
func (s *Server) ClientHandler() http.Handler { func (s *Server) ClientHandler() http.Handler {
router := chi.NewRouter() router := chi.NewRouter()
router.Use(cors.New(corsOptions).Handler) router.Use(cors.New(corsOptions).Handler)
s.attachClientEndpoints(router)
return router
}
func (s *Server) attachClientEndpoints(router *chi.Mux) {
router.Mount("/irma/", s.irmaserv.HandlerFunc()) router.Mount("/irma/", s.irmaserv.HandlerFunc())
if s.conf.StaticPath != "" { if s.conf.StaticPath != "" {
router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler()) router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
} }
router.Group(func(r chi.Router) {
return router if s.conf.Verbose >= 2 {
r.Use(s.logHandler("staticsession", true, true, true))
}
r.Post("/session/-/static/{name}", s.handleCreateStatic)
})
} }
// Handler returns a http.Handler that handles all IRMA requestor messages // Handler returns a http.Handler that handles all IRMA requestor messages
...@@ -180,10 +189,7 @@ func (s *Server) Handler() http.Handler { ...@@ -180,10 +189,7 @@ func (s *Server) Handler() http.Handler {
if !s.conf.separateClientServer() { if !s.conf.separateClientServer() {
// Mount server for irmaclient // Mount server for irmaclient
router.Mount("/irma/", s.irmaserv.HandlerFunc()) s.attachClientEndpoints(router)
if s.conf.StaticPath != "" {
router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
}
} }
router.NotFound(s.logHandler("requestor", false, true, true)(router.NotFoundHandler()).ServeHTTP) router.NotFound(s.logHandler("requestor", false, true, true)(router.NotFoundHandler()).ServeHTTP)
...@@ -353,6 +359,21 @@ func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) { ...@@ -353,6 +359,21 @@ func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
}) })
} }
func (s *Server) handleCreateStatic(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
rrequest := s.conf.staticSessions[name]
if rrequest == nil {
server.WriteError(w, server.ErrorInvalidRequest, "unknown static session")
return
}
qr, _, err := s.irmaserv.StartSession(rrequest, s.doResultCallback)
if err != nil {
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
server.WriteJson(w, qr)
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token")) res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
if res == nil { if res == nil {
...@@ -527,20 +548,42 @@ func (s *Server) resultJwt(sessionresult *server.SessionResult) (string, error) ...@@ -527,20 +548,42 @@ func (s *Server) resultJwt(sessionresult *server.SessionResult) (string, error)
func (s *Server) doResultCallback(result *server.SessionResult) { func (s *Server) doResultCallback(result *server.SessionResult) {
callbackUrl := s.irmaserv.GetRequest(result.Token).Base().CallbackUrl callbackUrl := s.irmaserv.GetRequest(result.Token).Base().CallbackUrl
if callbackUrl == "" || s.conf.jwtPrivateKey == nil { if callbackUrl == "" {
return return
} }
s.conf.Logger.WithFields(logrus.Fields{"session": result.Token, "callbackUrl": callbackUrl}).Debug("POSTing session result")
j, err := s.resultJwt(result) logger := s.conf.Logger.WithFields(logrus.Fields{"session": result.Token, "callbackUrl": callbackUrl})
if err != nil { if !strings.HasPrefix(callbackUrl, "https") {
_ = server.LogError(errors.WrapPrefix(err, "Failed to create JWT for result callback", 0)) if s.conf.Production {
return logger.Error("Not POSTing session result to callback URL without TLS: attributes would be unencrypted in transit")
return
} else {
logger.Warn("POSTing session result to callback URL without TLS: attributes are unencrypted in traffic")
}
} else {
logger.Debug("POSTing session result")
}
var res string
if s.conf.jwtPrivateKey != nil {
var err error
res, err = s.resultJwt(result)
if err != nil {
_ = server.LogError(errors.WrapPrefix(err, "Failed to create JWT for result callback", 0))
return
}
} else {
bts, err := json.Marshal(result)
if err != nil {
_ = server.LogError(errors.WrapPrefix(err, "Failed to marshal session result for result callback", 0))
return
}
res = string(bts)
} }
var x string // dummy for the server's return value that we don't care about var x string // dummy for the server's return value that we don't care about
if err := irma.NewHTTPTransport(callbackUrl).Post("", &x, j); err != nil { if err := irma.NewHTTPTransport(callbackUrl).Post("", &x, res); err != nil {
// not our problem, log it and go on // not our problem, log it and go on
s.conf.Logger.Warn(errors.WrapPrefix(err, "Failed to POST session result to callback URL", 0)) logger.Warn(errors.WrapPrefix(err, "Failed to POST session result to callback URL", 0))
} }
} }
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment