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 @@ -359,6 +359,32 @@ Additional types for session listing and notifications
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
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 { @@ -67,10 +67,10 @@ type DataSession struct {
Type string
Id string
Userid string `json:"Userid,omitempty"`
Ua string
Ua string `json:"Ua,omitempty"`
Token string `json:"Token,omitempty"`
Version string `json:"Version,omitempty"`
Rev uint64
Rev uint64 `json:"Rev,omitempty"`
Status interface{}
}
@ -105,16 +105,17 @@ type DataChatMessageStatus struct { @@ -105,16 +105,17 @@ type DataChatMessageStatus struct {
}
type DataIncoming struct {
Type string
Hello *DataHello
Offer *DataOffer
Candidate *DataCandidate
Answer *DataAnswer
Bye *DataBye
Status *DataStatus
Chat *DataChat
Conference *DataConference
Alive *DataAlive
Type string
Hello *DataHello
Offer *DataOffer
Candidate *DataCandidate
Answer *DataAnswer
Bye *DataBye
Status *DataStatus
Chat *DataChat
Conference *DataConference
Alive *DataAlive
Authentication *DataAuthentication
}
type DataOutgoing struct {
@ -140,3 +141,8 @@ type DataAlive struct { @@ -140,3 +141,8 @@ type DataAlive struct {
Type string
Alive uint64
}
type DataAuthentication struct {
Type string
Authentication *SessionToken
}

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

@ -28,6 +28,7 @@ import ( @@ -28,6 +28,7 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/securecookie"
"log"
@ -180,6 +181,22 @@ func (h *Hub) CreateSession(st *SessionToken) *Session { @@ -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) {
return h.tickets.Encode("token", st)
@ -302,7 +319,6 @@ func (h *Hub) unregisterHandler(c *Connection) { @@ -302,7 +319,6 @@ func (h *Hub) unregisterHandler(c *Connection) {
return
}
session := c.Session
c.close()
delete(h.connectionTable, c.Id)
delete(h.sessionTable, c.Id)
h.mutex.Unlock()
@ -311,6 +327,7 @@ func (h *Hub) unregisterHandler(c *Connection) { @@ -311,6 +327,7 @@ func (h *Hub) unregisterHandler(c *Connection) {
}
//log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id)
h.server.OnUnregister(c)
c.close()
}
@ -369,3 +386,22 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 { @@ -369,3 +386,22 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
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 { @@ -343,6 +343,7 @@ func runner(runtime phoenix.Runtime) error {
api.SetMux(r.PathPrefix("/api/v1/").Subrouter())
api.AddResource(&Rooms{}, "/rooms")
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens")
api.AddResource(&Sessions{hub: hub}, "/sessions/{id}/")
if statsEnabled {
api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats")
log.Println("Stats are enabled!")

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

@ -59,7 +59,7 @@ func (s *Server) OnUnregister(c *Connection) { @@ -59,7 +59,7 @@ func (s *Server) OnUnregister(c *Connection) {
//log.Println("OnUnregister", c.id)
if c.Hello {
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 {
//log.Println("Ingoring OnUnregister because of no Hello", c.Idx)
}
@ -86,13 +86,13 @@ func (s *Server) OnText(c *Connection, b Buffer) { @@ -86,13 +86,13 @@ func (s *Server) OnText(c *Connection, b Buffer) {
if c.Hello && c.Roomid != msg.Hello.Id {
// Room changed.
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
if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
c.Hello = 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 {
c.Hello = false
}
@ -109,13 +109,20 @@ func (s *Server) OnText(c *Connection, b Buffer) { @@ -109,13 +109,20 @@ func (s *Server) OnText(c *Connection, b Buffer) {
if c.h.config.defaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
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":
s.Unicast(c, msg.Bye.To, msg.Bye)
case "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) {
s.Broadcast(c, &DataSession{Type: "Status", Id: c.Id, Status: msg.Status.Status, Rev: rev})
s.Broadcast(c, c.Session.DataSessionStatus())
}
case "Chat":
// TODO(longsleep): Limit sent chat messages per incoming connection.
@ -219,6 +226,19 @@ func (s *Server) Users(c *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) {
rcu.Sessionid = c.Id

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

@ -22,9 +22,13 @@ @@ -22,9 +22,13 @@
package main
import (
"errors"
"github.com/gorilla/securecookie"
"sync"
)
var sessionNonces *securecookie.SecureCookie
type Session struct {
Id string
Sid string
@ -33,6 +37,7 @@ type Session struct { @@ -33,6 +37,7 @@ type Session struct {
Ua string
UpdateRev uint64
Status interface{}
Nonce string
mutex sync.RWMutex
}
@ -70,13 +75,64 @@ func (s *Session) Update(update *SessionUpdate) uint64 { @@ -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()
defer s.mutex.Unlock()
s.Id = st.Id
s.Sid = st.Sid
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 {
@ -98,6 +154,48 @@ func (s *Session) Data() *DataSession { @@ -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 {
Id string
Types []string
@ -110,4 +208,11 @@ type SessionToken struct { @@ -110,4 +208,11 @@ type SessionToken struct {
Id string
Sid 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 @@ @@ -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($, _) { @@ -26,6 +26,7 @@ define(['jquery', 'underscore'], function($, _) {
var Api = function(connector) {
this.id = null;
this.sid = null;
this.session = {};
this.connector = connector;
@ -112,6 +113,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -112,6 +113,7 @@ define(['jquery', 'underscore'], function($, _) {
this.connector.token = data.Token;
}
this.id = data.Id;
this.sid = data.Sid;
this.e.triggerHandler("received.self", [data]);
break;
case "Offer":
@ -225,6 +227,20 @@ define(['jquery', 'underscore'], function($, _) { @@ -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) {
var data = {

49
static/js/services/mediastream.js

@ -55,6 +55,55 @@ define([ @@ -55,6 +55,55 @@ define([
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 = {
version: version,
ws: url,

Loading…
Cancel
Save