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 (
"testing"
"time"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
"github.com/privacybydesign/irmago/server"
"github.com/privacybydesign/irmago/server/irmaserver"
......@@ -118,5 +119,18 @@ var JwtServerConfiguration = &requestorserver.Configuration{
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"),
}
package sessiontest
import (
"context"
"encoding/json"
"net/http"
"path/filepath"
"reflect"
"testing"
"path/filepath"
"time"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs"
......@@ -388,3 +391,41 @@ func TestDownloadSchemeManager(t *testing.T) {
require.NoError(t, err)
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
// newQrSession creates and starts a new interactive IRMA session
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
session := &session{
ServerURL: qr.URL,
......
......@@ -163,6 +163,7 @@ const (
ActionDisclosing = Action("disclosing")
ActionSigning = Action("signing")
ActionIssuing = Action("issuing")
ActionRedirect = Action("redirect")
ActionUnknown = Action("unknown")
)
......@@ -312,6 +313,7 @@ func (qr *Qr) Validate() (err error) {
case ActionDisclosing: // nop
case ActionIssuing: // nop
case ActionSigning: // nop
case ActionRedirect: // nop
default:
return errors.New("Unsupported session type")
}
......
......@@ -125,6 +125,7 @@ func setFlags(cmd *cobra.Command, production bool) error {
issHelp += " (default *)"
}
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.StringP("jwt-issuer", "j", "irmaserver", "JWT issuer")
......@@ -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")
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 {
if !viper.IsSet(typ) && (!viper.GetBool("production") || typ != "issue-perms") {
return []string{"*"}
......
......@@ -3,6 +3,7 @@ package requestorserver
import (
"crypto/rsa"
"crypto/tls"
"encoding/json"
"fmt"
"regexp"
"strconv"
......@@ -65,6 +66,9 @@ type Configuration struct {
// Host static files under this URL prefix
StaticPrefix string `json:"static_prefix" mapstructure:"static_prefix"`
StaticSessions map[string]interface{} `json:"static_sessions"`
staticSessions map[string]irma.RequestorRequest
jwtPrivateKey *rsa.PrivateKey
}
......@@ -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
}
......
......@@ -14,6 +14,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
......@@ -163,13 +164,21 @@ var corsOptions = cors.Options{
func (s *Server) ClientHandler() http.Handler {
router := chi.NewRouter()
router.Use(cors.New(corsOptions).Handler)
s.attachClientEndpoints(router)
return router
}
func (s *Server) attachClientEndpoints(router *chi.Mux) {
router.Mount("/irma/", s.irmaserv.HandlerFunc())
if s.conf.StaticPath != "" {
router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
}
return router
router.Group(func(r chi.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
......@@ -180,10 +189,7 @@ func (s *Server) Handler() http.Handler {
if !s.conf.separateClientServer() {
// Mount server for irmaclient
router.Mount("/irma/", s.irmaserv.HandlerFunc())
if s.conf.StaticPath != "" {
router.Mount(s.conf.StaticPrefix, s.StaticFilesHandler())
}
s.attachClientEndpoints(router)
}
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) {
})
}
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) {
res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token"))
if res == nil {
......@@ -527,20 +548,42 @@ func (s *Server) resultJwt(sessionresult *server.SessionResult) (string, error)
func (s *Server) doResultCallback(result *server.SessionResult) {
callbackUrl := s.irmaserv.GetRequest(result.Token).Base().CallbackUrl
if callbackUrl == "" || s.conf.jwtPrivateKey == nil {
if callbackUrl == "" {
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 !strings.HasPrefix(callbackUrl, "https") {
if s.conf.Production {
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
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
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