Skip to content

Commit b6d4bc6

Browse files
Merge pull request #287 from opentok/devx-8994-change-default-client-tokens-to-jwt
DEVX-8994: Change default client tokens to jwt
2 parents 128943a + 2fb3a18 commit b6d4bc6

File tree

11 files changed

+477
-150
lines changed

11 files changed

+477
-150
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
matrix:
1313
os: [ubuntu-latest, windows-latest, macos-latest]
14-
ruby: [2.5, 2.6, 2.7, 3.0]
14+
ruby: [3.0, 3.1, 3.2, 3.3]
1515
exclude:
1616
- os: windows-latest
1717
ruby: 3.0

CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 4.11.0
2+
3+
* Updating client token creation to use JWTs by default. See [#287](https://github.com/opentok/OpenTok-Ruby-SDK/pull/274)
4+
15
# 4.9.0
26

37
* Adds the `publisheronly` role for client token creation. See [#272](https://github.com/opentok/OpenTok-Ruby-SDK/pull/272)

Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
source "http://rubygems.org"
22

3+
gem "pry"
4+
35
# Specify your gem's dependencies in opentok.gemspec
46
gemspec

lib/opentok/opentok.rb

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module OpenTok
3636
# the token.
3737
#
3838
# @param [Hash] options A hash defining options for the token.
39+
# @option options [String] :token_type The type of token to generate. Must be one of 'T1' or 'JWT'. 'JWT' is the default.
3940
# @option options [Symbol] :role The role for the token. Set this to one of the following
4041
# values:
4142
# * <code>:subscriber</code> -- A subscriber can only subscribe to streams.

lib/opentok/session.rb

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module OpenTok
2828
# Generates a token.
2929
#
3030
# @param [Hash] options
31+
# @option options [String] :token_type The type of token to generate. Must be one of 'T1' or 'JWT'. 'JWT' is the default.
3132
# @option options [Symbol] :role The role for the token. Set this to one of the following
3233
# values:
3334
# * <code>:subscriber</code> -- A subscriber can only subscribe to streams.

lib/opentok/token_generator.rb

+60-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
require "openssl"
77
require "active_support"
88
require "active_support/time"
9+
require "jwt"
910

1011
module OpenTok
1112
# @private
1213
module TokenGenerator
14+
VALID_TOKEN_TYPES = ['T1', 'JWT'].freeze
15+
1316
# this works when using include TokenGenerator
1417
def self.included(base)
1518
base.extend(ClassMethods)
@@ -33,7 +36,14 @@ def generates_tokens(arg_lambdas={})
3336
end
3437
dynamic_args.compact!
3538
args = args.first(4-dynamic_args.length)
36-
self.class.generate_token.call(*dynamic_args, *args)
39+
token_type = if args.any? && args.last.is_a?(Hash) && args.last.has_key?(:token_type)
40+
args.last[:token_type].upcase
41+
else
42+
"JWT"
43+
end
44+
raise "'#{token_type}' is not a valid token type. Must be one of: #{VALID_TOKEN_TYPES.join(', ')}" unless VALID_TOKEN_TYPES.include? token_type
45+
46+
self.class.generate_token(token_type).call(*dynamic_args, *args)
3747
end
3848
end
3949

@@ -43,14 +53,14 @@ def arg_lambdas
4353
end
4454

4555
# Generates a token
46-
def generate_token
47-
TokenGenerator::GENERATE_TOKEN_LAMBDA
56+
def generate_token(token_type)
57+
token_type == 'T1' ? TokenGenerator::GENERATE_T1_TOKEN_LAMBDA : TokenGenerator::GENERATE_JWT_LAMBDA
4858
end
4959

5060
end
5161

5262
# @private TODO: this probably doesn't need to be a constant anyone can read
53-
GENERATE_TOKEN_LAMBDA = ->(api_key, api_secret, session_id, opts = {}) do
63+
GENERATE_T1_TOKEN_LAMBDA = ->(api_key, api_secret, session_id, opts = {}) do
5464
# normalize required data params
5565
role = opts.fetch(:role, :publisher)
5666
unless ROLES.has_key? role
@@ -101,6 +111,52 @@ def generate_token
101111
TOKEN_SENTINEL + Base64.strict_encode64(meta_string + ":" + data_string)
102112
end
103113

114+
GENERATE_JWT_LAMBDA = ->(api_key, api_secret, session_id, opts = {}) do
115+
# normalize required data params
116+
role = opts.fetch(:role, :publisher)
117+
unless ROLES.has_key? role
118+
raise "'#{role}' is not a recognized role"
119+
end
120+
unless Session.belongs_to_api_key? session_id.to_s, api_key
121+
raise "Cannot generate token for a session_id that doesn't belong to api_key: #{api_key}"
122+
end
123+
124+
# minimum data params
125+
data_params = {
126+
:iss => api_key,
127+
:ist => "project",
128+
:iat => Time.now.to_i,
129+
:nonce => Random.rand,
130+
:role => role,
131+
:scope => "session.connect",
132+
:session_id => session_id,
133+
}
134+
135+
# normalize and add additional data params
136+
unless (expire_time = opts[:expire_time].to_i) == 0
137+
unless expire_time.between?(Time.now.to_i, (Time.now + 30.days).to_i)
138+
raise "Expire time must be within the next 30 days"
139+
end
140+
data_params[:exp] = expire_time.to_i
141+
end
142+
143+
unless opts[:data].nil?
144+
unless (data = opts[:data].to_s).length < 1000
145+
raise "Connection data must be less than 1000 characters"
146+
end
147+
data_params[:connection_data] = data
148+
end
149+
150+
if opts[:initial_layout_class_list]
151+
if opts[:initial_layout_class_list].is_a?(Array)
152+
data_params[:initial_layout_class_list] = opts[:initial_layout_class_list].join(' ')
153+
else
154+
data_params[:initial_layout_class_list] = opts[:initial_layout_class_list].to_s
155+
end
156+
end
157+
158+
JWT.encode(data_params, api_secret, 'HS256', header_fields={typ: 'JWT'})
159+
end
104160

105161
# this works when using extend TokenGenerator
106162
# def generates_tokens(method_opts)

lib/opentok/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
module OpenTok
22
# @private
3-
VERSION = '4.9.0'
3+
VERSION = '4.11.0'
44
end

spec/matchers/token.rb

+34-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
require "base64"
44
require "openssl"
55
require "addressable/uri"
6+
require "jwt"
67

7-
RSpec::Matchers.define :carry_token_data do |input_data|
8+
RSpec::Matchers.define :carry_t1_token_data do |input_data|
89
option_to_token_key = {
910
:api_key => :partner_id,
1011
:data => :connection_data,
@@ -37,7 +38,7 @@
3738
end
3839
end
3940

40-
RSpec::Matchers.define :carry_valid_token_signature do |api_secret|
41+
RSpec::Matchers.define :carry_valid_t1_token_signature do |api_secret|
4142
match do |token|
4243
decoded_token = Base64.decode64(token[4..token.length])
4344
metadata, data_string = decoded_token.split(':')
@@ -48,3 +49,34 @@
4849
signature == OpenSSL::HMAC.hexdigest(digest, api_secret, data_string)
4950
end
5051
end
52+
53+
RSpec::Matchers.define :carry_jwt_token_data do |input_data|
54+
match do |token|
55+
decoded_token = JWT.decode(token, nil, false)
56+
token_data = decoded_token.first.transform_keys {|k| k.to_sym}.transform_values {|v| v.to_s}
57+
check_token_data = lambda { |key, value|
58+
if token_data.has_key? key
59+
unless value.nil?
60+
return token_data[key] == value.to_s
61+
end
62+
return true
63+
end
64+
false
65+
}
66+
unless input_data.respond_to? :all?
67+
return check_token_data.call(input_data, nil)
68+
end
69+
input_data.all? { |k, v| check_token_data.call(k, v) }
70+
end
71+
end
72+
73+
RSpec::Matchers.define :carry_valid_jwt_token_signature do |api_secret|
74+
match do |token|
75+
begin
76+
JWT.decode(token, api_secret, true)
77+
rescue
78+
return false
79+
end
80+
true
81+
end
82+
end

spec/opentok/session_spec.rb

-1
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,4 @@
7272
end
7373
include_examples "session generates tokens"
7474
end
75-
7675
end

0 commit comments

Comments
 (0)