Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEVX-8994: Change default client tokens to JWT #274

Merged
merged 8 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
ruby: [2.5, 2.6, 2.7, 3.0]
ruby: [3.0, 3.1, 3.2, 3.3]
exclude:
- os: windows-latest
ruby: 3.0
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 4.10.0

* Updating client token creation to use JWTs by default. See [#274](https://github.com/opentok/OpenTok-Ruby-SDK/pull/274)

# 4.9.0

* Adds the `publisheronly` role for client token creation. See [#272](https://github.com/opentok/OpenTok-Ruby-SDK/pull/272)
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
source "http://rubygems.org"

gem "pry"

# Specify your gem's dependencies in opentok.gemspec
gemspec
1 change: 1 addition & 0 deletions lib/opentok/opentok.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module OpenTok
# the token.
#
# @param [Hash] options A hash defining options for the token.
# @option options [String] :token_type The type of token to generate. Must be one of 'T1' or 'JWT'. 'JWT' is the default.
# @option options [Symbol] :role The role for the token. Set this to one of the following
# values:
# * <code>:subscriber</code> -- A subscriber can only subscribe to streams.
Expand Down
1 change: 1 addition & 0 deletions lib/opentok/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module OpenTok
# Generates a token.
#
# @param [Hash] options
# @option options [String] :token_type The type of token to generate. Must be one of 'T1' or 'JWT'. 'JWT' is the default.
# @option options [Symbol] :role The role for the token. Set this to one of the following
# values:
# * <code>:subscriber</code> -- A subscriber can only subscribe to streams.
Expand Down
65 changes: 61 additions & 4 deletions lib/opentok/token_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
require "openssl"
require "active_support"
require "active_support/time"
require "jwt"

module OpenTok
# @private
module TokenGenerator
VALID_TOKEN_TYPES = ['T1', 'JWT'].freeze

# this works when using include TokenGenerator
def self.included(base)
base.extend(ClassMethods)
Expand All @@ -33,7 +36,14 @@ def generates_tokens(arg_lambdas={})
end
dynamic_args.compact!
args = args.first(4-dynamic_args.length)
self.class.generate_token.call(*dynamic_args, *args)
token_type = if args.any? && args.last.is_a?(Hash) && args.last.has_key?(:token_type)
args.last[:token_type].upcase
else
"JWT"
end
raise "'#{token_type}' is not a valid token type. Must be one of: #{VALID_TOKEN_TYPES.join(', ')}" unless VALID_TOKEN_TYPES.include? token_type

self.class.generate_token(token_type).call(*dynamic_args, *args)
end
end

Expand All @@ -43,14 +53,14 @@ def arg_lambdas
end

# Generates a token
def generate_token
TokenGenerator::GENERATE_TOKEN_LAMBDA
def generate_token(token_type)
token_type == 'T1' ? TokenGenerator::GENERATE_T1_TOKEN_LAMBDA : TokenGenerator::GENERATE_JWT_LAMBDA
end

end

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

GENERATE_JWT_LAMBDA = ->(api_key, api_secret, session_id, opts = {}) do
# normalize required data params
role = opts.fetch(:role, :publisher)
unless ROLES.has_key? role
raise "'#{role}' is not a recognized role"
end
unless Session.belongs_to_api_key? session_id.to_s, api_key
raise "Cannot generate token for a session_id that doesn't belong to api_key: #{api_key}"
end

# minimum data params
data_params = {
:iss => api_key,
:ist => "project",
:iat => Time.now.to_i,
:exp => Time.now.to_i + 86400,
:nonce => Random.rand,
:role => role,
:scope => "session.connect",
:session_id => session_id,
}

# normalize and add additional data params
unless (expire_time = opts[:expire_time].to_i) == 0
unless expire_time.between?(Time.now.to_i, (Time.now + 30.days).to_i)
raise "Expire time must be within the next 30 days"
end
data_params[:exp] = expire_time.to_i
end

unless opts[:data].nil?
unless (data = opts[:data].to_s).length < 1000
raise "Connection data must be less than 1000 characters"
end
data_params[:connection_data] = data
end

if opts[:initial_layout_class_list]
if opts[:initial_layout_class_list].is_a?(Array)
data_params[:initial_layout_class_list] = opts[:initial_layout_class_list].join(' ')
else
data_params[:initial_layout_class_list] = opts[:initial_layout_class_list].to_s
end
end

JWT.encode(data_params, api_secret, 'HS256', header_fields={typ: 'JWT'})
end

# this works when using extend TokenGenerator
# def generates_tokens(method_opts)
Expand Down
2 changes: 1 addition & 1 deletion lib/opentok/version.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module OpenTok
# @private
VERSION = '4.9.0'
VERSION = '4.10.0'
end
36 changes: 34 additions & 2 deletions spec/matchers/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
require "base64"
require "openssl"
require "addressable/uri"
require "jwt"

RSpec::Matchers.define :carry_token_data do |input_data|
RSpec::Matchers.define :carry_t1_token_data do |input_data|
option_to_token_key = {
:api_key => :partner_id,
:data => :connection_data,
Expand Down Expand Up @@ -37,7 +38,7 @@
end
end

RSpec::Matchers.define :carry_valid_token_signature do |api_secret|
RSpec::Matchers.define :carry_valid_t1_token_signature do |api_secret|
match do |token|
decoded_token = Base64.decode64(token[4..token.length])
metadata, data_string = decoded_token.split(':')
Expand All @@ -48,3 +49,34 @@
signature == OpenSSL::HMAC.hexdigest(digest, api_secret, data_string)
end
end

RSpec::Matchers.define :carry_jwt_token_data do |input_data|
match do |token|
decoded_token = JWT.decode(token, nil, false)
token_data = decoded_token.first.transform_keys {|k| k.to_sym}.transform_values {|v| v.to_s}
check_token_data = lambda { |key, value|
if token_data.has_key? key
unless value.nil?
return token_data[key] == value.to_s
end
return true
end
false
}
unless input_data.respond_to? :all?
return check_token_data.call(input_data, nil)
end
input_data.all? { |k, v| check_token_data.call(k, v) }
end
end

RSpec::Matchers.define :carry_valid_jwt_token_signature do |api_secret|
match do |token|
begin
JWT.decode(token, api_secret, true)
rescue
return false
end
true
end
end
1 change: 0 additions & 1 deletion spec/opentok/session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,4 @@
end
include_examples "session generates tokens"
end

end
Loading
Loading