Browse Source

Implemented user creation API.

pull/28/head
Simon Eisenmann 11 years ago
parent
commit
10f85891ac
  1. 79
      doc/REST-API.txt
  2. 50
      doc/plugin-test-authorize.js
  3. 9
      server.conf.in
  4. 7
      src/app/spreed-speakfreely-server/main.go
  5. 12
      src/app/spreed-speakfreely-server/sessions.go
  6. 98
      src/app/spreed-speakfreely-server/users.go

79
doc/REST-API.txt

@ -14,20 +14,6 @@ API or there was a problem while JSON encoding. @@ -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: @@ -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

50
doc/plugin-test-authorize.js

@ -26,6 +26,8 @@ define(['angular', 'sjcl'], function(angular, sjcl) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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);
};
}]);
}

9
server.conf.in

@ -51,7 +51,7 @@ listen = 127.0.0.1:8080 @@ -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 @@ -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 @@ -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

7
src/app/spreed-speakfreely-server/main.go

@ -270,9 +270,6 @@ func runner(runtime phoenix.Runtime) error { @@ -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 { @@ -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 { @@ -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")

12
src/app/spreed-speakfreely-server/sessions.go

@ -77,6 +77,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H @@ -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 @@ -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.

98
src/app/spreed-speakfreely-server/users.go

@ -25,9 +25,13 @@ import ( @@ -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 ( @@ -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, @@ -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 { @@ -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 { @@ -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"}}
}

Loading…
Cancel
Save