Browse Source

Implemented web client integration of user authentication, authorization and creation.

pull/28/head
Simon Eisenmann 11 years ago
parent
commit
f27c5d10a3
  1. 74
      doc/plugin-test-authorize.js
  2. 6
      server.conf.in
  3. 44
      src/app/spreed-speakfreely-server/config.go
  4. 6
      src/app/spreed-speakfreely-server/hub.go
  5. 31
      src/app/spreed-speakfreely-server/main.go
  6. 53
      src/app/spreed-speakfreely-server/users.go
  7. 73
      static/js/controllers/mediastreamcontroller.js
  8. 2439
      static/js/libs/sjcl.js
  9. 58
      static/js/services/mediastream.js

74
doc/plugin-test-authorize.js

@ -60,88 +60,42 @@ define(['angular', 'sjcl'], function(angular, sjcl) {
}; };
$window.testCreateSuseridServer = function() { $window.testCreateSuseridServer = function() {
mediaStream.users.register(function(data) {
var url = mediaStream.url.api("users"); lastNonce = data.nonce;
console.log("URL", url); lastUserid = data.userid;
var data = { lastUseridCombo = data.useridcombo;
id: mediaStream.api.id, lastSecret = data.secret;
sid: mediaStream.api.sid console.log("Retrieved user", data);
} }, function() {
console.log("Data", data); console.log("Register error", arguments);
$.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) { $window.testAuthorize = function(useridCombo, secret) {
console.log("Testing authorize with userid", useridCombo, secret); console.log("Testing authorize with userid", useridCombo, secret);
var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/"; mediaStream.users.authorize(useridCombo, secret, function(data) {
console.log("URL", url); lastNonce = data.nonce;
var data = { lastUserid = data.userid;
id: mediaStream.api.id, console.log("Retrieved nonce", data);
sid: mediaStream.api.sid, }, function() {
useridcombo: useridCombo, console.log("Authorize error", arguments);
secret: secret
}
console.log("Data", data);
$.ajax({
type: "PATCH",
url: url,
contentType: "application/json",
dataType: "json",
data: JSON.stringify(data),
success: function(data) {
if (data.success) {
lastNonce = data.nonce;
lastUserid = data.userid;
console.log("Retrieved nonce", data);
}
},
error: function() {
console.log("error", arguments)
}
}); });
}; };
$window.testLastAuthenticate = function() { $window.testLastAuthenticate = function() {
if (!lastNonce || !lastUserid) { if (!lastNonce || !lastUserid) {
console.log("Run testAuthorize first."); console.log("Run testAuthorize first.");
return return
} }
mediaStream.api.requestAuthentication(lastUserid, lastNonce); mediaStream.api.requestAuthentication(lastUserid, lastNonce);
}; };
$window.testLastAuthorize = function() { $window.testLastAuthorize = function() {
if (!lastUseridCombo || !lastSecret) { if (!lastUseridCombo || !lastSecret) {
console.log("Run testCreateSuseridServer fist."); console.log("Run testCreateSuseridServer fist.");
return return
} }
$window.testAuthorize(lastUseridCombo, lastSecret); $window.testAuthorize(lastUseridCombo, lastSecret);
}; };
}]); }]);

6
server.conf.in

@ -71,6 +71,10 @@ sessionSecret = the-default-secret-do-not-keep-me
; all users will join this room if enabled. If it is disabled then a room join ; all users will join this room if enabled. If it is disabled then a room join
; form will be shown instead. ; form will be shown instead.
;defaultRoomEnabled = true ;defaultRoomEnabled = true
; Server token is a public random string which is used to enhance security of
; server generated security tokens. When the serverToken is changed all existing
; nonces become invalid. Use 32 or 64 byte random data.
;serverToken = i-did-not-change-the-public-token-boo
; Full path to an extra templates directory. Templates in this directory ending ; Full path to an extra templates directory. Templates in this directory ending
; with .html will be parsed on startup and can be used to fill the supported ; with .html will be parsed on startup and can be used to fill the supported
; extra-* template slots. If the extra folder has a sub folder "static", the ; extra-* template slots. If the extra folder has a sub folder "static", the
@ -89,7 +93,7 @@ sessionSecret = the-default-secret-do-not-keep-me
[users] [users]
; Set to true to enable user functionality. ; Set to true to enable user functionality.
;enabled = false enabled = false
; Set authorization mode for users. Currently implemented is the "sharedsecret" ; Set authorization mode for users. Currently implemented is the "sharedsecret"
; mode which does validate the userid with a HMAC authentication secret. ; mode which does validate the userid with a HMAC authentication secret.
; The format goes like this: ; The format goes like this:

44
src/app/spreed-speakfreely-server/config.go

@ -26,20 +26,38 @@ import (
) )
type Config struct { type Config struct {
Title string // Title Title string // Title
ver string // Version (not exported to Javascript) ver string // Version (not exported to Javascript)
S string // Static URL prefix with version S string // Static URL prefix with version
B string // Base URL B string // Base URL
StunURIs []string // STUN server URIs Token string // Server token
TurnURIs []string // TURN server URIs StunURIs []string // STUN server URIs
Tokens bool // True when we got a tokens file TurnURIs []string // TURN server URIs
Version string // Server version number Tokens bool // True when we got a tokens file
globalRoomid string // Id of the global room (not exported to Javascript) Version string // Server version number
defaultRoomEnabled bool // Flag to enable default room ("") UsersEnabled bool // Flag if users are enabled
Plugin string // Plugin to load UsersAllowRegistration bool // Flag if users can register
Plugin string // Plugin to load
globalRoomid string // Id of the global room (not exported to Javascript)
defaultRoomEnabled bool // Flag if default room ("") is enabled
} }
func NewConfig(title, ver, runtimeVersion, basePath string, stunURIs, turnURIs []string, tokens bool, globalRoomid string, defaultRoomEnabled bool, plugin string) *Config { func NewConfig(title, ver, runtimeVersion, basePath, serverToken string, stunURIs, turnURIs []string, tokens bool, globalRoomid string, defaultRoomEnabled, usersEnabled, usersAllowRegistration bool, plugin string) *Config {
sv := fmt.Sprintf("static/ver=%s", ver) sv := fmt.Sprintf("static/ver=%s", ver)
return &Config{Title: title, ver: ver, S: sv, B: basePath, StunURIs: stunURIs, TurnURIs: turnURIs, Tokens: tokens, Version: runtimeVersion, globalRoomid: globalRoomid, defaultRoomEnabled: defaultRoomEnabled, Plugin: plugin} return &Config{
Title: title,
ver: ver,
S: sv,
B: basePath,
Token: serverToken,
StunURIs: stunURIs,
TurnURIs: turnURIs,
Tokens: tokens,
Version: runtimeVersion,
UsersEnabled: usersEnabled,
UsersAllowRegistration: usersAllowRegistration,
Plugin: plugin,
globalRoomid: globalRoomid,
defaultRoomEnabled: defaultRoomEnabled,
}
} }

6
src/app/spreed-speakfreely-server/hub.go

@ -174,7 +174,11 @@ func (h *Hub) CreateSession(st *SessionToken) *Session {
session = NewSession(id, sid, "") session = NewSession(id, sid, "")
log.Println("Created new session id", len(id), id, sid) log.Println("Created new session id", len(id), id, sid)
} else { } else {
session = NewSession(st.Id, st.Sid, st.Userid) userid := st.Userid
if !h.config.UsersEnabled {
userid = ""
}
session = NewSession(st.Id, st.Sid, userid)
} }
return session return session

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

@ -263,6 +263,24 @@ func runner(runtime phoenix.Runtime) error {
defaultRoomEnabled = defaultRoomEnabledString == "true" defaultRoomEnabled = defaultRoomEnabledString == "true"
} }
serverToken, err := runtime.GetString("app", "serverToken")
if err == nil {
//TODO(longsleep): When we have a database, generate this once from random source and store it.
serverToken = "i-did-not-change-the-public-token-boo"
}
usersEnabled := false
usersEnabledString, err := runtime.GetString("users", "enabled")
if err == nil {
usersEnabled = usersEnabledString == "true"
}
usersAllowRegistration := false
usersAllowRegistrationString, err := runtime.GetString("users", "allowRegistration")
if err == nil {
usersAllowRegistration = usersAllowRegistrationString == "true"
}
// Create token provider. // Create token provider.
var tokenProvider TokenProvider var tokenProvider TokenProvider
if tokenFile != "" { if tokenFile != "" {
@ -271,7 +289,7 @@ func runner(runtime phoenix.Runtime) error {
} }
// Create configuration data structure. // Create configuration data structure.
config = NewConfig(title, ver, runtimeVersion, basePath, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, plugin) config = NewConfig(title, ver, runtimeVersion, basePath, serverToken, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, usersEnabled, usersAllowRegistration, plugin)
// Load templates. // Load templates.
tt := template.New("") tt := template.New("")
@ -298,9 +316,6 @@ func runner(runtime phoenix.Runtime) error {
// Create our hub instance. // Create our hub instance.
hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret) hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret)
// Create Users handler.
users := NewUsers(hub, runtime)
// Set number of go routines if it is 1 // Set number of go routines if it is 1
if goruntime.GOMAXPROCS(0) == 1 { if goruntime.GOMAXPROCS(0) == 1 {
nCPU := goruntime.NumCPU() nCPU := goruntime.NumCPU()
@ -346,9 +361,13 @@ func runner(runtime phoenix.Runtime) error {
api.SetMux(r.PathPrefix("/api/v1/").Subrouter()) api.SetMux(r.PathPrefix("/api/v1/").Subrouter())
api.AddResource(&Rooms{}, "/rooms") api.AddResource(&Rooms{}, "/rooms")
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens")
if users.Enabled { if usersEnabled {
// Create Users handler.
users := NewUsers(hub, runtime)
api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/")
api.AddResource(users, "/users") if usersAllowRegistration {
api.AddResource(users, "/users")
}
} }
if statsEnabled { if statsEnabled {
api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats")

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

@ -99,55 +99,32 @@ type UserNonce struct {
type Users struct { type Users struct {
hub *Hub hub *Hub
Enabled bool
Create bool
Handler UsersHandler Handler UsersHandler
} }
func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users { func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users {
enabled := false
enabledString, err := runtime.GetString("users", "enabled")
if err == nil {
enabled = enabledString == "true"
}
create := false
createString, err := runtime.GetString("users", "allowRegistration")
if err == nil {
create = createString == "true"
}
var handler UsersHandler var handler UsersHandler
if enabled { mode, _ := runtime.GetString("users", "mode")
switch mode {
mode, _ := runtime.GetString("users", "mode") case "sharedsecret":
switch mode { secret, _ := runtime.GetString("users", "sharedsecret_secret")
case "sharedsecret": if secret != "" {
secret, _ := runtime.GetString("users", "sharedsecret_secret") handler = &UsersSharedsecretHandler{secret: []byte(secret)}
if secret != "" {
handler = &UsersSharedsecretHandler{secret: []byte(secret)}
}
default:
mode = ""
}
if handler == nil {
enabled = false
} else {
log.Printf("Enabled users handler '%s'\n", mode)
if create {
log.Println("Enabled users registration")
}
} }
default:
mode = ""
}
if handler == nil {
handler = &UsersSharedsecretHandler{secret: []byte("")}
} }
log.Printf("Enabled users handler '%s'\n", mode)
return &Users{ return &Users{
hub: hub, hub: hub,
Enabled: enabled,
Create: create,
Handler: handler, Handler: handler,
} }
@ -156,10 +133,6 @@ func NewUsers(hub *Hub, runtime phoenix.Runtime) *Users {
// Post is used to create new userids for this server. // Post is used to create new userids for this server.
func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) { 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) decoder := json.NewDecoder(request.Body)
var snr SessionNonceRequest var snr SessionNonceRequest
err := decoder.Decode(&snr) err := decoder.Decode(&snr)

73
static/js/controllers/mediastreamcontroller.js

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigScreen, moment) { define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function(_, BigScreen, moment, sjcl) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload) { return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload) {
@ -130,6 +130,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
// Default scope data. // Default scope data.
$scope.status = "initializing"; $scope.status = "initializing";
$scope.id = null; $scope.id = null;
$scope.userid = null;
$scope.peer = null; $scope.peer = null;
$scope.dialing = null; $scope.dialing = null;
$scope.conference = null; $scope.conference = null;
@ -153,6 +154,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
language: "" language: ""
} }
}; };
$scope.withStoredLogin = false;
// Data voids. // Data voids.
var cache = {}; var cache = {};
@ -365,9 +367,11 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
var reloadDialog = false; var reloadDialog = false;
mediaStream.api.e.on("received.self", function(event, data) { mediaStream.api.e.on("received.self", function(event, data) {
$timeout.cancel(ttlTimeout); $timeout.cancel(ttlTimeout);
safeApply($scope, function(scope) { safeApply($scope, function(scope) {
scope.id = scope.myid = data.Id; scope.id = scope.myid = data.Id;
scope.userid = data.Userid;
scope.turn = data.Turn; scope.turn = data.Turn;
scope.stun = data.Stun; scope.stun = data.Stun;
scope.refreshWebrtcSettings(); scope.refreshWebrtcSettings();
@ -385,6 +389,66 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
}, 300); }, 300);
} }
} }
// Support authentication.
if (!data.Userid && mediaStream.config.UsersEnabled) {
var key = mediaStream.config.Token;
// Check if we have something in store.
var login = localStorage.getItem("mediastream-login");
if (login) {
safeApply($scope, function(scope) {
scope.withStoredLogin = true;
});
try {
login = sjcl.decrypt(key, login);
login = JSON.parse(login)
} catch(err) {
console.error("Failed to parse login data", err);
login = {};
}
console.log("Trying to authorize with stored credentials ...");
switch (login.v) {
case 1:
var useridCombo = login.a;
var secret = login.b;
var expiry = login.t;
if (useridCombo && secret) {
mediaStream.users.authorize(useridCombo, secret, function(data) {
console.info("Retrieved nonce - authenticating as user:", data.userid);
mediaStream.api.requestAuthentication(data.userid, data.nonce);
delete data.nonce;
}, function(data, status) {
console.error("Failed to authorize session", status, data);
});
}
break;
default:
console.warn("Unknown stored credentials", login.v);
break
}
}
if (!login && mediaStream.config.UsersAllowRegistration) {
console.log("No userid - creating one ...");
mediaStream.users.register(function(data) {
var login = sjcl.encrypt(key, JSON.stringify({
v: 1,
t: data.timestamp || "",
a: data.useridcombo,
b: data.secret,
}));
localStorage.setItem("mediastream-login", login);
console.info("Created new userid:", data.userid);
mediaStream.api.requestAuthentication(data.userid, data.nonce);
delete data.nonce;
}, function(data, status) {
console.error("Failed to create userid", status, data);
});
}
}
// Support to upgrade stuff when ttl was reached. // Support to upgrade stuff when ttl was reached.
if (data.Turn.ttl) { if (data.Turn.ttl) {
ttlTimeout = $timeout(function() { ttlTimeout = $timeout(function() {
@ -519,6 +583,7 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
if (opts.soft) { if (opts.soft) {
return; return;
} }
$scope.userid = null;
break; break;
case "error": case "error":
if (reconnecting || connected) { if (reconnecting || connected) {
@ -593,6 +658,12 @@ define(['underscore', 'bigscreen', 'moment', 'webrtc.adapter'], function(_, BigS
} }
}); });
$scope.$watch("userid", function(userid) {
if (userid) {
console.info("Session is now authenticated:", userid);
}
});
// Apply all layout stuff as classes to our element. // Apply all layout stuff as classes to our element.
$scope.$watch("layout", (function() { $scope.$watch("layout", (function() {
var makeName = function(prefix, n) { var makeName = function(prefix, n) {

2439
static/js/libs/sjcl.js

File diff suppressed because it is too large Load Diff

58
static/js/services/mediastream.js

@ -60,6 +60,64 @@ define([
return (context.Cfg.B || "/") + "api/v1/" + path; return (context.Cfg.B || "/") + "api/v1/" + path;
} }
}, },
users: {
register: function(success_cb, error_cb) {
var url = mediaStream.url.api("users");
var data = {
id: mediaStream.api.id,
sid: mediaStream.api.sid
}
$http({
method: "POST",
url: url,
data: JSON.stringify(data),
headers: {'Content-Type': 'application/json'}
}).
success(function(data, status) {
if (data.userid !== "" && data.success) {
success_cb(data, status);
} else {
if (error_cb) {
error_cb(data, status);
}
}
}).
error(function(data, status) {
if (error_cb) {
error_cb(data, status)
}
});
},
authorize: function(useridCombo, secret, success_cb, error_cb) {
var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/";
var data = {
id: mediaStream.api.id,
sid: mediaStream.api.sid,
useridcombo: useridCombo,
secret: secret
}
$http({
method: "PATCH",
url: url,
data: JSON.stringify(data),
headers: {'Content-Type': 'application/json'}
}).
success(function(data, status) {
if (data.nonce !== "" && data.success) {
success_cb(data, status);
} else {
if (error_cb) {
error_cb(data, status);
}
}
}).
error(function(data, status) {
if (error_cb) {
error_cb(data, status)
}
});
}
},
initialize: function($rootScope, translation) { initialize: function($rootScope, translation) {
var cont = false; var cont = false;

Loading…
Cancel
Save