From 10f85891ac0a326d1925806e6d2b0eb31c26b432 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Fri, 25 Apr 2014 21:57:30 +0200 Subject: [PATCH] Implemented user creation API. --- doc/REST-API.txt | 79 ++++++++++----- doc/plugin-test-authorize.js | 50 +++++++++- server.conf.in | 9 +- src/app/spreed-speakfreely-server/main.go | 7 +- src/app/spreed-speakfreely-server/sessions.go | 12 +-- src/app/spreed-speakfreely-server/users.go | 98 +++++++++++++++++-- 6 files changed, 211 insertions(+), 44 deletions(-) diff --git a/doc/REST-API.txt b/doc/REST-API.txt index 3d6cac85..54cb4e54 100644 --- a/doc/REST-API.txt +++ b/doc/REST-API.txt @@ -14,20 +14,6 @@ API or there was a problem while JSON encoding. Available end points with request methods and content-type: - /api/v1/rooms - - The rooms end point can be used to generate new random room ids. - - POST application/x-www-form-urlencoded - No parameters. - Response 200: - { - "success": true, - "name": "room-name", - "url": "https://yourserver/room-name" - } - - /api/v1/tokens The tokens end point is to validate client side access tokens. @@ -47,29 +33,76 @@ Available end points with request methods and content-type: } - /api/v1/sessions/{id}/ + /api/v1/rooms + + The rooms end point can be used to generate new random room ids. + + POST application/x-www-form-urlencoded + No parameters. + Response 200: + { + "success": true, + "name": "room-name", + "url": "https://yourserver/room-name" + } + + + /api/v1/sessions - The sessions end point is for session interaction like authorization and - can only be used with a session id passed in as subpath. Make sure to - provide the trailing slash (/). + The sessions end point is for session interaction like authorization. - PATCH application/json + /api/v1/sessions/{id}/ + + A session id is passed in as subpath. Make sure to add the trailing slash (/). + + PATCH application/json + { + id: "session-id", + sid: "secure-session-id", + useridcombo: "authorization-id", + secret: "secret-for-this-user-id" + } + Response 200: + { + "success": true, + "userid": "user-id-for-nonce", + "nonce": "authorization-nonce" + } + Response 403: + { + "success": false, + "code": "error-code", + "message": "error-message" + } + Response 404 text/plain: + Returned when users are disabled on the server. + + + /api/v1/users + + The users end point is for user interaction like registration. + + POST application/json { - Id: "session-id", - Sid: "secure-session-id", - Userid: "user-id-to-authorize" + id: "session-id", + sid: "secure-session-id" } Response 200: { "success": true, + "userid": "user-id", + "useridcombo": "authorization-id", + "secret": "authorization-secret-for-authorization-id", "nonce": "authorization-nonce" } - Response 403: + Response 400, 403: { "success": false, "code": "error-code", "message": "error-message" } + Response 404 text/plain: + Returned when user registration is disabled on the server. /api/v1/stats diff --git a/doc/plugin-test-authorize.js b/doc/plugin-test-authorize.js index 339e345a..3da49345 100644 --- a/doc/plugin-test-authorize.js +++ b/doc/plugin-test-authorize.js @@ -26,6 +26,8 @@ define(['angular', 'sjcl'], function(angular, sjcl) { var lastNonce = null; var lastUserid = null; + var lastUseridCombo = null; + var lastSecret = null; var disconnectTimeout = null; app.run(["$window", "mediaStream", function($window, mediaStream) { @@ -46,7 +48,7 @@ define(['angular', 'sjcl'], function(angular, sjcl) { console.info("Started disconnector."); }; - $window.testCreateSuserid = function(key, userid) { + $window.testCreateSuseridLocal = function(key, userid) { var k = sjcl.codec.utf8String.toBits(key); var foo = new sjcl.misc.hmac(k, sjcl.hash.sha256) @@ -57,6 +59,37 @@ define(['angular', 'sjcl'], function(angular, sjcl) { }; + $window.testCreateSuseridServer = function() { + + var url = mediaStream.url.api("users"); + console.log("URL", url); + var data = { + id: mediaStream.api.id, + sid: mediaStream.api.sid + } + console.log("Data", data); + $.ajax({ + type: "POST", + url: url, + contentType: "application/json", + dataType: "json", + data: JSON.stringify(data), + success: function(data) { + if (data.success) { + lastNonce = data.nonce; + lastUserid = data.userid; + lastUseridCombo = data.useridcombo; + lastSecret = data.secret; + console.log("Retrieved user", data); + } + }, + error: function() { + console.log("error", arguments) + } + }); + + }; + $window.testAuthorize = function(useridCombo, secret) { console.log("Testing authorize with userid", useridCombo, secret); @@ -79,7 +112,7 @@ define(['angular', 'sjcl'], function(angular, sjcl) { if (data.success) { lastNonce = data.nonce; lastUserid = data.userid; - console.log("Retrieved nonce", lastNonce, lastUserid); + console.log("Retrieved nonce", data); } }, error: function() { @@ -89,7 +122,7 @@ define(['angular', 'sjcl'], function(angular, sjcl) { }; - $window.testAuthenticate = function() { + $window.testLastAuthenticate = function() { if (!lastNonce || !lastUserid) { console.log("Run testAuthorize first."); @@ -100,6 +133,17 @@ define(['angular', 'sjcl'], function(angular, sjcl) { }; + $window.testLastAuthorize = function() { + + if (!lastUseridCombo || !lastSecret) { + console.log("Run testCreateSuseridServer fist."); + return + } + + $window.testAuthorize(lastUseridCombo, lastSecret); + + }; + }]); } diff --git a/server.conf.in b/server.conf.in index b4b92ecb..b7007509 100644 --- a/server.conf.in +++ b/server.conf.in @@ -51,7 +51,7 @@ listen = 127.0.0.1:8080 ; cannot be established without a TURN server due to firewall reasons. An open ; source TURN server which is fully supported can be found at ; https://code.google.com/p/rfc5766-turn-server/. -;turnURIs = turn:turnserver:port?transport=udp turns:turnserver:443?transport=tcp +;turnURIs = turn:turnserver:port?transport=udp ; Shared secret authentication for TURN user generation if the TURN server is ; protected (which it should be). ; See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 for details. @@ -61,7 +61,8 @@ listen = 127.0.0.1:8080 ; are recommented. sessionSecret = the-default-secret-do-not-keep-me ; Full path to a text file containig client tokens which a user needs to enter -; when accessing the web client. Each line in this file represents a valid token. +; when accessing the web client. Each line in this file represents a valid +; token. ;tokenFile = tokens.txt ; The name of a global room. If enabled it should be kept secret. Users in that ; room are visible in all other rooms. @@ -97,3 +98,7 @@ sessionSecret = the-default-secret-do-not-keep-me ; The shared secred for HMAC validation in "sharedsecret" mode. Best use 32 or ; 64 bytes of random data. ;sharedsecret_secret = some-secret-do-not-keep +; The server can create new userids if enabled. Set allowRegistration to true to +; enable userid creation/registration. Users are created to match the settings +; of the currently configured mode (see above). +;allowRegistration = false diff --git a/src/app/spreed-speakfreely-server/main.go b/src/app/spreed-speakfreely-server/main.go index ce827ad7..4d175543 100644 --- a/src/app/spreed-speakfreely-server/main.go +++ b/src/app/spreed-speakfreely-server/main.go @@ -270,9 +270,6 @@ func runner(runtime phoenix.Runtime) error { tokenProvider = TokenFileProvider(tokenFile) } - // Create Users handler. - users := NewUsers(runtime) - // Create configuration data structure. config = NewConfig(title, ver, runtimeVersion, basePath, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, plugin) @@ -301,6 +298,9 @@ func runner(runtime phoenix.Runtime) error { // Create our hub instance. hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret) + // Create Users handler. + users := NewUsers(hub, runtime) + // Set number of go routines if it is 1 if goruntime.GOMAXPROCS(0) == 1 { nCPU := goruntime.NumCPU() @@ -348,6 +348,7 @@ func runner(runtime phoenix.Runtime) error { api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") if users.Enabled { api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") + api.AddResource(users, "/users") } if statsEnabled { api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") diff --git a/src/app/spreed-speakfreely-server/sessions.go b/src/app/spreed-speakfreely-server/sessions.go index f6c19182..425fe2b5 100644 --- a/src/app/spreed-speakfreely-server/sessions.go +++ b/src/app/spreed-speakfreely-server/sessions.go @@ -77,6 +77,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H log.Println("Session patch failed - sid empty.") } + // Make sure Sid matches session and is valid. + if !sessions.hub.ValidateSession(snr.Id, snr.Sid) { + log.Println("Session patch failed - validation failed.") + error = true + } + // Validate with users handler. userid, err := sessions.users.Handler.Validate(&snr) if err != nil { @@ -90,12 +96,6 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H log.Println("Session patch failed - userid empty.") } - // Make sure Sid matches session. - if !sessions.hub.ValidateSession(snr.Id, snr.Sid) { - log.Println("Session patch failed - validation failed.") - error = true - } - var nonce string if !error { // FIXME(longsleep): Not running this might reveal error state with a timing attack. diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go index e3589945..b6aa18a0 100644 --- a/src/app/spreed-speakfreely-server/users.go +++ b/src/app/spreed-speakfreely-server/users.go @@ -25,9 +25,13 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/json" "errors" + "fmt" + "github.com/satori/go.uuid" "github.com/strukturag/phoenix" "log" + "net/http" "strconv" "strings" "time" @@ -35,12 +39,21 @@ import ( type UsersHandler interface { Validate(snr *SessionNonceRequest) (string, error) + Create(snr *UserNonce) (*UserNonce, error) } type UsersSharedsecretHandler struct { secret []byte } +func (uh *UsersSharedsecretHandler) createHMAC(useridCombo string) string { + + m := hmac.New(sha256.New, uh.secret) + m.Write([]byte(useridCombo)) + return base64.StdEncoding.EncodeToString(m.Sum(nil)) + +} + func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest) (string, error) { // Parse UseridCombo. @@ -57,23 +70,41 @@ func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest) (string, return "", errors.New("expired secret") } - // Check HMAC. - foo := hmac.New(sha256.New, uh.secret) - foo.Write([]byte(snr.UseridCombo)) - fooSecret := base64.StdEncoding.EncodeToString(foo.Sum(nil)) - if snr.Secret != fooSecret { + secret := uh.createHMAC(snr.UseridCombo) + if snr.Secret != secret { return "", errors.New("invalid secret") } return userid, nil + +} + +func (uh *UsersSharedsecretHandler) Create(un *UserNonce) (*UserNonce, error) { + + // TODO(longsleep): Make this configureable - One year for now ... + expiration := time.Now().Add(time.Duration(1) * time.Hour * 24 * 31 * 12) + un.UseridCombo = fmt.Sprintf("%d:%s", expiration.Unix(), un.Userid) + un.Secret = uh.createHMAC(un.UseridCombo) + return un, nil + +} + +type UserNonce struct { + Nonce string `json:"nonce"` + Userid string `json:"userid"` + UseridCombo string `json:"useridcombo"` + Secret string `json:"secret"` + Success bool `json:"success"` } type Users struct { + hub *Hub Enabled bool + Create bool Handler UsersHandler } -func NewUsers(runtime phoenix.Runtime) *Users { +func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users { enabled := false enabledString, err := runtime.GetString("users", "enabled") @@ -81,6 +112,12 @@ func NewUsers(runtime phoenix.Runtime) *Users { enabled = enabledString == "true" } + create := false + createString, err := runtime.GetString("users", "allowRegistration") + if err == nil { + create = createString == "true" + } + var handler UsersHandler if enabled { @@ -99,14 +136,61 @@ func NewUsers(runtime phoenix.Runtime) *Users { if handler == nil { enabled = false } else { - log.Printf("Enabled users handler '%s'.\n", mode) + log.Printf("Enabled users handler '%s'\n", mode) + if create { + log.Println("Enabled users registration") + } } } return &Users{ + hub: hub, Enabled: enabled, + Create: create, Handler: handler, } } + +// Post is used to create new userids for this server. +func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) { + + if !users.Create { + return 404, "404 page not found", http.Header{"Content-Type": {"text/plain"}} + } + + decoder := json.NewDecoder(request.Body) + var snr SessionNonceRequest + err := decoder.Decode(&snr) + if err != nil { + return 400, NewApiError("users_bad_request", "Failed to parse request"), http.Header{"Content-Type": {"application/json"}} + } + + // Make sure that we have a Sid. + if snr.Sid == "" || snr.Id == "" { + return 400, NewApiError("users_bad_request", "Incomplete request"), http.Header{"Content-Type": {"application/json"}} + } + + // Do this before session validation to avoid timing information. + userid := uuid.NewV4().String() + + // Make sure Sid matches session and is valid. + if !users.hub.ValidateSession(snr.Id, snr.Sid) { + return 403, NewApiError("users_invalid_session", "Invalid session"), http.Header{"Content-Type": {"application/json"}} + } + + nonce, err := users.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid}) + if err != nil { + return 400, NewApiError("users_request_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}} + } + + un, err := users.Handler.Create(&UserNonce{Nonce: nonce, Userid: userid, Success: true}) + if err != nil { + return 400, NewApiError("users_create_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}} + } + + log.Printf("Users create successfull %s -> %s\n", snr.Id, un.Userid) + return 200, un, http.Header{"Content-Type": {"application/json"}} + +}