Browse Source

Implemented user authorization and authentication api and data layers.

pull/28/head
Simon Eisenmann 11 years ago committed by Simon Eisenmann
parent
commit
8a74cfb892
  1. 26
      doc/CHANNELING-API.txt
  2. 30
      src/app/spreed-speakfreely-server/channeling.go
  3. 38
      src/app/spreed-speakfreely-server/hub.go
  4. 1
      src/app/spreed-speakfreely-server/main.go
  5. 30
      src/app/spreed-speakfreely-server/server.go
  6. 107
      src/app/spreed-speakfreely-server/session.go
  7. 95
      src/app/spreed-speakfreely-server/sessions.go
  8. 16
      static/js/mediastream/api.js
  9. 49
      static/js/services/mediastream.js

26
doc/CHANNELING-API.txt

@ -359,6 +359,32 @@ Additional types for session listing and notifications
The Alive value is a timestamp integer in milliseconds (unix time). The Alive value is a timestamp integer in milliseconds (unix time).
User authorization and session authentication
The channeling API supports an Authentication document to bind and existing
session to a given user. The required information to do this cannot be
received through the channeling API. It depends on the server configuration
how the Nonce and Userid are generated/validated.
Authentication
{
"Type": "Authentication",
"Authentication": {
"Userid": "53",
"Nonce": "nonce-for-this-session-and-userid"
}
}
The Authentication document binds a userid to the current session. The
Nonce and Userid need to be validateable by the server. If Authentication
was successfull, a new Self document will be sent. The Nonce value can
be generated by using the REST API (sessions end point).
There is no way to undo authentication for a session. For log out, close
the session (disconnect) and forget the token.
Chat messages and status information Chat messages and status information
The chat is used to transfer simple messages ore more complex structures The chat is used to transfer simple messages ore more complex structures

30
src/app/spreed-speakfreely-server/channeling.go

@ -67,10 +67,10 @@ type DataSession struct {
Type string Type string
Id string Id string
Userid string `json:"Userid,omitempty"` Userid string `json:"Userid,omitempty"`
Ua string Ua string `json:"Ua,omitempty"`
Token string `json:"Token,omitempty"` Token string `json:"Token,omitempty"`
Version string `json:"Version,omitempty"` Version string `json:"Version,omitempty"`
Rev uint64 Rev uint64 `json:"Rev,omitempty"`
Status interface{} Status interface{}
} }
@ -105,16 +105,17 @@ type DataChatMessageStatus struct {
} }
type DataIncoming struct { type DataIncoming struct {
Type string Type string
Hello *DataHello Hello *DataHello
Offer *DataOffer Offer *DataOffer
Candidate *DataCandidate Candidate *DataCandidate
Answer *DataAnswer Answer *DataAnswer
Bye *DataBye Bye *DataBye
Status *DataStatus Status *DataStatus
Chat *DataChat Chat *DataChat
Conference *DataConference Conference *DataConference
Alive *DataAlive Alive *DataAlive
Authentication *DataAuthentication
} }
type DataOutgoing struct { type DataOutgoing struct {
@ -140,3 +141,8 @@ type DataAlive struct {
Type string Type string
Alive uint64 Alive uint64
} }
type DataAuthentication struct {
Type string
Authentication *SessionToken
}

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

@ -28,6 +28,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"log" "log"
@ -180,6 +181,22 @@ func (h *Hub) CreateSession(st *SessionToken) *Session {
} }
func (h *Hub) ValidateSession(id, sid string) bool {
var decoded string
err := h.tickets.Decode("id", id, &decoded)
if err != nil {
log.Println("Session validation error", err, id, sid)
return false
}
if decoded != sid {
log.Println("Session validation failed", id, sid)
return false
}
return true
}
func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) { func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) {
return h.tickets.Encode("token", st) return h.tickets.Encode("token", st)
@ -302,7 +319,6 @@ func (h *Hub) unregisterHandler(c *Connection) {
return return
} }
session := c.Session session := c.Session
c.close()
delete(h.connectionTable, c.Id) delete(h.connectionTable, c.Id)
delete(h.sessionTable, c.Id) delete(h.sessionTable, c.Id)
h.mutex.Unlock() h.mutex.Unlock()
@ -311,6 +327,7 @@ func (h *Hub) unregisterHandler(c *Connection) {
} }
//log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id) //log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id)
h.server.OnUnregister(c) h.server.OnUnregister(c)
c.close()
} }
@ -369,3 +386,22 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
return rev return rev
} }
func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) {
h.mutex.RLock()
c, ok := h.connectionTable[st.Id]
h.mutex.RUnlock()
if !ok {
return "", errors.New("no such connection")
}
nonce, err := c.Session.Authorize(st)
if err != nil {
return "", err
}
return nonce, nil
}

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

@ -343,6 +343,7 @@ 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")
api.AddResource(&Sessions{hub: hub}, "/sessions/{id}/")
if statsEnabled { if statsEnabled {
api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats")
log.Println("Stats are enabled!") log.Println("Stats are enabled!")

30
src/app/spreed-speakfreely-server/server.go

@ -59,7 +59,7 @@ func (s *Server) OnUnregister(c *Connection) {
//log.Println("OnUnregister", c.id) //log.Println("OnUnregister", c.id)
if c.Hello { if c.Hello {
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid})
s.Broadcast(c, &DataSession{Type: "Left", Id: c.Id, Status: "hard"}) s.Broadcast(c, c.Session.DataSessionLeft("hard"))
} else { } else {
//log.Println("Ingoring OnUnregister because of no Hello", c.Idx) //log.Println("Ingoring OnUnregister because of no Hello", c.Idx)
} }
@ -86,13 +86,13 @@ func (s *Server) OnText(c *Connection, b Buffer) {
if c.Hello && c.Roomid != msg.Hello.Id { if c.Hello && c.Roomid != msg.Hello.Id {
// Room changed. // Room changed.
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid})
s.Broadcast(c, &DataSession{Type: "Left", Id: c.Id, Status: "soft"}) s.Broadcast(c, c.Session.DataSessionLeft("soft"))
} }
c.Roomid = msg.Hello.Id c.Roomid = msg.Hello.Id
if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
c.Hello = true c.Hello = true
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true}) s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true})
s.Broadcast(c, &DataSession{Type: "Joined", Id: c.Id, Ua: msg.Hello.Ua}) s.Broadcast(c, c.Session.DataSessionJoined())
} else { } else {
c.Hello = false c.Hello = false
} }
@ -109,13 +109,20 @@ func (s *Server) OnText(c *Connection, b Buffer) {
if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
s.Users(c) s.Users(c)
} }
case "Authentication":
if s.Authenticate(c, msg.Authentication.Authentication) {
s.OnRegister(c)
if c.Hello {
s.Broadcast(c, c.Session.DataSessionStatus())
}
}
case "Bye": case "Bye":
s.Unicast(c, msg.Bye.To, msg.Bye) s.Unicast(c, msg.Bye.To, msg.Bye)
case "Status": case "Status":
//log.Println("Status", msg.Status) //log.Println("Status", msg.Status)
rev := s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
s.Broadcast(c, &DataSession{Type: "Status", Id: c.Id, Status: msg.Status.Status, Rev: rev}) s.Broadcast(c, c.Session.DataSessionStatus())
} }
case "Chat": case "Chat":
// TODO(longsleep): Limit sent chat messages per incoming connection. // TODO(longsleep): Limit sent chat messages per incoming connection.
@ -219,6 +226,19 @@ func (s *Server) Users(c *Connection) {
} }
func (s *Server) Authenticate(c *Connection, st *SessionToken) bool {
err := c.Session.Authenticate(st)
if err == nil {
log.Println("Authentication success", c.Id, c.Idx, st.Userid)
return true
} else {
log.Println("Authentication failed", err, c.Id, c.Idx, st.Userid, st.Nonce)
return false
}
}
func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) { func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) {
rcu.Sessionid = c.Id rcu.Sessionid = c.Id

107
src/app/spreed-speakfreely-server/session.go

@ -22,9 +22,13 @@
package main package main
import ( import (
"errors"
"github.com/gorilla/securecookie"
"sync" "sync"
) )
var sessionNonces *securecookie.SecureCookie
type Session struct { type Session struct {
Id string Id string
Sid string Sid string
@ -33,6 +37,7 @@ type Session struct {
Ua string Ua string
UpdateRev uint64 UpdateRev uint64
Status interface{} Status interface{}
Nonce string
mutex sync.RWMutex mutex sync.RWMutex
} }
@ -70,13 +75,64 @@ func (s *Session) Update(update *SessionUpdate) uint64 {
} }
func (s *Session) Apply(st *SessionToken) { func (s *Session) Apply(st *SessionToken) uint64 {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
s.Id = st.Id s.Id = st.Id
s.Sid = st.Sid
s.Userid = st.Userid s.Userid = st.Userid
s.UpdateRev++
return s.UpdateRev
}
func (s *Session) Authorize(st *SessionToken) (string, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.Id != st.Id || s.Sid != st.Sid {
return "", errors.New("session id mismatch")
}
if s.Userid != "" {
return "", errors.New("session already authenticated")
}
// Create authentication nonce.
var err error
s.Nonce, err = sessionNonces.Encode(s.Sid, st.Userid)
return s.Nonce, err
}
func (s *Session) Authenticate(st *SessionToken) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.Userid != "" {
return errors.New("session already authenticated")
}
if s.Nonce == "" || s.Nonce != st.Nonce {
return errors.New("nonce validation failed")
}
var userid string
err := sessionNonces.Decode(s.Sid, st.Nonce, &userid)
if err != nil {
return err
}
if st.Userid != userid {
return errors.New("user id mismatch")
}
s.Nonce = ""
s.Userid = st.Userid
s.UpdateRev++
return nil
} }
func (s *Session) Token() *SessionToken { func (s *Session) Token() *SessionToken {
@ -98,6 +154,48 @@ func (s *Session) Data() *DataSession {
} }
func (s *Session) DataSessionLeft(state string) *DataSession {
s.mutex.RLock()
defer s.mutex.RUnlock()
return &DataSession{
Type: "Left",
Id: s.Id,
Status: state,
}
}
func (s *Session) DataSessionJoined() *DataSession {
s.mutex.RLock()
defer s.mutex.RUnlock()
return &DataSession{
Type: "Joined",
Id: s.Id,
Userid: s.Userid,
Ua: s.Ua,
}
}
func (s *Session) DataSessionStatus() *DataSession {
s.mutex.RLock()
defer s.mutex.RUnlock()
return &DataSession{
Type: "Status",
Id: s.Id,
Userid: s.Userid,
Status: s.Status,
Rev: s.UpdateRev,
}
}
type SessionUpdate struct { type SessionUpdate struct {
Id string Id string
Types []string Types []string
@ -110,4 +208,11 @@ type SessionToken struct {
Id string Id string
Sid string Sid string
Userid string Userid string
Nonce string `json:"Nonce,omitempty"`
}
func init() {
// Create nonce generator.
sessionNonces = securecookie.New(securecookie.GenerateRandomKey(64), nil)
sessionNonces.MaxAge(60)
} }

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

@ -0,0 +1,95 @@
/*
* Spreed Speak Freely.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed Speak Freely.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"encoding/json"
"github.com/gorilla/mux"
"net/http"
)
type SessionNonce struct {
Nonce string `json:"nonce"`
Success bool `json:"success"`
}
type Sessions struct {
hub *Hub
}
// Patch is used to add a userid to a given session (login).
func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.Header) {
// Make sure to always run all the checks to make timing attacks harder.
error := false
decoder := json.NewDecoder(request.Body)
var st SessionToken
err := decoder.Decode(&st)
if err != nil {
error = true
}
vars := mux.Vars(request)
id, ok := vars["id"]
if !ok {
error = true
}
// Make sure data matches request.
if id != st.Id {
error = true
}
// Make sure that we have a Sid.
if st.Sid == "" {
error = true
}
// Make sure that we have a user.
if st.Userid == "" {
error = true
}
// TODO(longsleep): Validate userid.
// Make sure Sid matches session.
if !sessions.hub.ValidateSession(st.Id, st.Sid) {
error = true
}
var nonce string
if !error {
// FIXME(longsleep): Not running this might releal error state with a timing attack.
nonce, err = sessions.hub.sessiontokenHandler(&st)
if err != nil {
error = true
}
}
if error {
return 403, NewApiError("session_patch_failed", "Failed to patch session"), nil
}
return 200, &SessionNonce{Nonce: nonce, Success: true}, http.Header{"Content-Type": {"application/json"}}
}

16
static/js/mediastream/api.js

@ -26,6 +26,7 @@ define(['jquery', 'underscore'], function($, _) {
var Api = function(connector) { var Api = function(connector) {
this.id = null; this.id = null;
this.sid = null;
this.session = {}; this.session = {};
this.connector = connector; this.connector = connector;
@ -112,6 +113,7 @@ define(['jquery', 'underscore'], function($, _) {
this.connector.token = data.Token; this.connector.token = data.Token;
} }
this.id = data.Id; this.id = data.Id;
this.sid = data.Sid;
this.e.triggerHandler("received.self", [data]); this.e.triggerHandler("received.self", [data]);
break; break;
case "Offer": case "Offer":
@ -225,6 +227,20 @@ define(['jquery', 'underscore'], function($, _) {
}; };
Api.prototype.requestAuthentication = function(userid, nonce) {
var data = {
Type: "Authentication",
Authentication: {
Userid: userid,
Nonce: nonce
}
}
return this.send("Authentication", data);
};
Api.prototype.updateStatus = function(status) { Api.prototype.updateStatus = function(status) {
var data = { var data = {

49
static/js/services/mediastream.js

@ -55,6 +55,55 @@ define([
console.info("Started disconnector."); console.info("Started disconnector.");
}; };
(function() {
var lastNonce = null;
var lastUserid = null;
$window.testAuthorize = function(userid) {
console.log("Testing authorize with userid", userid);
var url = mediaStream.url.api("sessions") + "/" + api.id + "/";
console.log("URL", url);
var data = {
Id: api.id,
Sid: api.sid,
Userid: userid
}
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 = userid;
console.log("Retrieved nonce", lastNonce, lastUserid);
}
},
error: function() {
console.log("error", arguments)
}
});
};
$window.testAuthenticate = function() {
if (!lastNonce || !lastUserid) {
console.log("Run testAuthorize first.");
return
}
api.requestAuthentication(lastUserid, lastNonce);
};
}())
var mediaStream = { var mediaStream = {
version: version, version: version,
ws: url, ws: url,

Loading…
Cancel
Save