Browse Source

Refactoring of Jitsi-Meet integration : better organisation and context from jitsi token support

Guénaël Muller 6 years ago
parent
commit
226ccaef4b

+ 15 - 32
tracim/tracim/controllers/jitsi_meet.py View File

1
+import typing
1
 import tg
2
 import tg
2
 from tg import abort
3
 from tg import abort
3
 from tg import expose
4
 from tg import expose
6
 from tracim.lib.predicates import current_user_is_reader
7
 from tracim.lib.predicates import current_user_is_reader
7
 from sqlalchemy.orm.exc import NoResultFound
8
 from sqlalchemy.orm.exc import NoResultFound
8
 
9
 
9
-from tracim.lib.utils import str_as_alpha_num_str
10
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiMeetRoom
11
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiTokenConfig
10
+from tracim.lib.jitsi_meet.room import JitsiMeetRoom
11
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
12
 from tracim.config.app_cfg import CFG
12
 from tracim.config.app_cfg import CFG
13
 
13
 
14
+from tracim.model.data import User
14
 from tracim.model.serializers import Context, CTX, DictLikeClass
15
 from tracim.model.serializers import Context, CTX, DictLikeClass
15
 from tracim.controllers import TIMRestController, TIMRestPathContextSetup
16
 from tracim.controllers import TIMRestController, TIMRestPathContextSetup
16
 
17
 
25
         except NoResultFound:
26
         except NoResultFound:
26
             abort(404)
27
             abort(404)
27
 
28
 
28
-
29
     @tg.require(current_user_is_reader())
29
     @tg.require(current_user_is_reader())
30
     @expose('tracim.templates.videoconf.jitsi_meet')
30
     @expose('tracim.templates.videoconf.jitsi_meet')
31
     def get(self):
31
     def get(self):
32
-        return self._process()
32
+        user = tmpl_context.current_user
33
+        return self._jitsi_room(jitsi_user=user)
33
 
34
 
34
     @tg.require(current_user_is_reader())
35
     @tg.require(current_user_is_reader())
35
     @expose('tracim.templates.videoconf.invite')
36
     @expose('tracim.templates.videoconf.invite')
36
     def invite(self):
37
     def invite(self):
37
-        return self._process()
38
+        return self._jitsi_room()
38
 
39
 
39
-    def _process(self):
40
+    @classmethod
41
+    def _jitsi_room(
42
+            cls,
43
+            jitsi_user: typing.Union[JitsiMeetUser, User, None]=None,
44
+    ):
40
         cfg = CFG.get_instance()
45
         cfg = CFG.get_instance()
41
         if not cfg.JITSI_MEET_ACTIVATED:
46
         if not cfg.JITSI_MEET_ACTIVATED:
42
             abort(404)
47
             abort(404)
50
         dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace,
55
         dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace,
51
                                                             'workspace')
56
                                                             'workspace')
52
 
57
 
53
-        # TODO - G.M - 18-01-2017 -
54
-        # allow to set specific room name from workspace object ?
55
-        room = "{uuid}{workspace_id}{workspace_label}".format(
56
-            uuid=cfg.TRACIM_INSTANCE_UUID,
57
-            workspace_id=workspace.workspace_id,
58
-            workspace_label=workspace.label)
59
-
60
-        # Jitsi-Meet doesn't like specials_characters
61
-        room = str_as_alpha_num_str(room)
62
-
63
-        token = None
64
-        if cfg.JITSI_MEET_USE_TOKEN:
65
-            if cfg.JITSI_MEET_TOKEN_GENERATOR == 'local':
66
-                token = JitsiTokenConfig(
67
-                    app_id=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_APP_ID,
68
-                    secret=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_SECRET,
69
-                    alg=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_ALG,
70
-                    duration=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_DURATION,
71
-                )
72
-            else:
73
-                abort(400)
74
-
75
         jitsi_meet_room = JitsiMeetRoom(
58
         jitsi_meet_room = JitsiMeetRoom(
76
-            room=room,
77
-            domain=cfg.JITSI_MEET_DOMAIN,
78
-            token_config=token)
59
+            issuer=jitsi_user,
60
+            receivers=workspace,
61
+        )
79
 
62
 
80
         return DictLikeClass(fake_api=fake_api,
63
         return DictLikeClass(fake_api=fake_api,
81
                              result=dictified_workspace,
64
                              result=dictified_workspace,

+ 0 - 83
tracim/tracim/lib/jitsi_meet/jitsi_meet.py View File

1
-import datetime
2
-import typing
3
-
4
-import jwt
5
-
6
-class NoTokenConfigError(Exception):
7
-    pass
8
-
9
-class JitsiTokenConfig(object):
10
-
11
-    def __init__(self,
12
-                 app_id: str,
13
-                 secret: str,
14
-                 alg: str,
15
-                 duration: int,
16
-                 )-> None:
17
-        """
18
-        JWT token generator config for JitsiMeet,
19
-        :param app_id: application identifier
20
-        :param secret: secret share between token generator and XMPP server
21
-        :param alg: algorithm used
22
-        :param duration: duration of token
23
-        """
24
-        self.app_id = app_id
25
-        self.secret = secret
26
-        self.alg = alg
27
-        self.duration = duration
28
-
29
-
30
-class JitsiMeetRoom(object):
31
-
32
-    def __init__(self,
33
-                 domain: str,
34
-                 room: str,
35
-                 token_config: typing.Optional[JitsiTokenConfig],
36
-                 ) -> None:
37
-        """
38
-        JitsiMeet room Parameters
39
-        :param domain: jitsi-meet domain
40
-        :param room: room name
41
-        :param token_config: token config, None if token not used.
42
-        """
43
-        self.domain = domain
44
-        self.room = room
45
-        self.token_config = token_config
46
-
47
-    def generate_token(self) -> str:
48
-        """
49
-        Create jwt token according to room name and config
50
-        see https://github.com/jitsi/lib-jitsi-meet/blob/master/doc/tokens.md
51
-        :return: jwt encoded token as string
52
-        """
53
-        if not self.token_config:
54
-            raise NoTokenConfigError
55
-        now = datetime.datetime.utcnow()
56
-        exp = now+datetime.timedelta(seconds=self.token_config.duration)
57
-        data = {
58
-            "iss": self.token_config.app_id,  # Issuer
59
-            "room": self.room,  # Custom-param for jitsi_meet
60
-            "aud": "*",  # TODO: Understood this param
61
-            "exp": exp,  # Expiration date
62
-            "nbf": now,  # NotBefore
63
-            "iat": now,   # IssuedAt
64
-        }
65
-        jwt_token = jwt.encode(data,
66
-                               self.token_config.secret,
67
-                               algorithm=self.token_config.alg)
68
-        return jwt_token.decode("utf-8")
69
-
70
-    def generate_url(self) -> str:
71
-        """
72
-        Generate url with or without token
73
-        :return: url as string
74
-        """
75
-        if self.token_config:
76
-            token = self.generate_token()
77
-            url = "{}/{}?jwt={}".format(self.domain,
78
-                                        self.room,
79
-                                        token,)
80
-        else:
81
-            url = "{}/{}".format(self.domain,
82
-                                 self.room,)
83
-        return "https://{}".format(url)

+ 124 - 0
tracim/tracim/lib/jitsi_meet/room.py View File

1
+import typing
2
+from tracim.lib.jitsi_meet.token import JitsiMeetToken
3
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
4
+from tracim.lib.jitsi_meet.token import JitsiMeetContext
5
+from tracim.lib.utils import str_as_alpha_num_str
6
+from tracim.config.app_cfg import CFG
7
+from tracim.model.data import Workspace
8
+from tracim.model.data import User
9
+import uuid
10
+
11
+
12
+class JitsiMeetNoTokenGenerator(Exception):
13
+    pass
14
+
15
+
16
+class JitsiMeetTokenNotActivated(Exception):
17
+    pass
18
+
19
+
20
+class JitsiMeetRoom(object):
21
+    def __init__(
22
+            self,
23
+            receivers: Workspace,
24
+            issuer: typing.Union[User, JitsiMeetUser, None]=None,
25
+    ) -> None:
26
+        """
27
+        :param issuer: user who initiated Jitsi Meet talk
28
+        if None, default user is created. Can be both Tracim User or
29
+        JitsiMeetUser.
30
+        :param receivers: User or Room who can talk with sender. Now, only
31
+        Workspace are supported.
32
+        """
33
+        self._set_domain()
34
+        self._set_token_params()
35
+        self._set_context(
36
+            receivers=receivers,
37
+            issuer=issuer,
38
+        )
39
+        self.room = self._generate_room_name(receivers)
40
+
41
+    def _set_domain(self):
42
+        cfg = CFG.get_instance()
43
+        self.domain = cfg.JITSI_MEET_DOMAIN
44
+
45
+    def _set_token_params(self):
46
+        cfg = CFG.get_instance()
47
+        self.use_token = cfg.JITSI_MEET_USE_TOKEN
48
+        if self.use_token:
49
+            if cfg.JITSI_MEET_TOKEN_GENERATOR == 'local':
50
+                self.token_app_id = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_APP_ID  # nopep8
51
+                self.token_secret = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_SECRET  # nopep8
52
+                self.token_alg = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_ALG
53
+                self.token_duration = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_DURATION  # nopep8
54
+            else:
55
+                raise JitsiMeetNoTokenGenerator
56
+
57
+    def _set_context(
58
+            self,
59
+            receivers: Workspace,
60
+            issuer: typing.Union[User, JitsiMeetUser, None],
61
+    ):
62
+
63
+        # User
64
+        if isinstance(issuer, JitsiMeetUser):
65
+            user = issuer
66
+        elif isinstance(issuer, User):
67
+            user = JitsiMeetUser(
68
+                name=issuer.display_name,
69
+                avatar_url=None,
70
+                jitsi_meet_user_id=issuer.display_name,
71
+            )
72
+        else:
73
+            user = JitsiMeetUser(
74
+                jitsi_meet_user_id=str(uuid.uuid4()),
75
+            )
76
+
77
+        # Group
78
+        group = receivers.label
79
+
80
+        self.context = JitsiMeetContext(
81
+            user=user,
82
+            group=group,
83
+        )
84
+
85
+    @classmethod
86
+    def _generate_room_name(cls, workspace: Workspace):
87
+        cfg = CFG.get_instance()
88
+        room = "{uuid}{workspace_id}{workspace_label}".format(
89
+            uuid=cfg.TRACIM_INSTANCE_UUID,
90
+            workspace_id=workspace.workspace_id,
91
+            workspace_label=workspace.label)
92
+
93
+        # Jitsi-Meet doesn't like specials_characters
94
+        return str_as_alpha_num_str(room)
95
+
96
+    def generate_token(self) -> str:
97
+        if not self.use_token:
98
+            raise JitsiMeetTokenNotActivated
99
+
100
+        token = JitsiMeetToken(
101
+            domain=self.domain,
102
+            room=self.room,
103
+            app_id=self.token_app_id,
104
+            secret=self.token_secret,
105
+            alg=self.token_alg,
106
+            duration=self.token_duration,
107
+            context=self.context,
108
+        )
109
+        return token.generate()
110
+
111
+    def generate_url(self, token=None) -> str:
112
+        """
113
+        Generate url with or without token
114
+        :return: url as string
115
+        """
116
+        if token:
117
+            url = "{}/{}?jwt={}".format(self.domain,
118
+                                        self.room,
119
+                                        token)
120
+        else:
121
+            url = "{}/{}".format(self.domain,
122
+                                 self.room,
123
+                                 )
124
+        return "https://{}".format(url)

+ 117 - 0
tracim/tracim/lib/jitsi_meet/token.py View File

1
+import datetime
2
+import typing
3
+import jwt
4
+
5
+
6
+class JitsiMeetUser(object):
7
+
8
+    def __init__(
9
+            self,
10
+            jitsi_meet_user_id: str,
11
+            name: typing.Optional[str] = None,
12
+            email: typing.Optional[str] = None,
13
+            avatar_url: typing.Optional[str] = None,
14
+    ):
15
+        """
16
+
17
+        :param avatar_url: url for user avatar_url
18
+        :param name: display name of user
19
+        :param email: email of user
20
+        :param jitsi_meet_user_id: Jitsi-Meet id of user
21
+        """
22
+        self.avatar_url = avatar_url
23
+        self.name = name
24
+        self.email = email
25
+        self.jitsi_meet_user_id = jitsi_meet_user_id
26
+
27
+    def as_dict(self) -> dict:
28
+        data = {
29
+            'id': self.jitsi_meet_user_id,
30
+        }
31
+        if self.name:
32
+            data['name'] = self.name
33
+        if self.email:
34
+            data['email'] = self.email
35
+        if self.avatar_url:
36
+            data['avatar'] = self.avatar_url
37
+        return data
38
+
39
+
40
+class JitsiMeetContext(object):
41
+
42
+    def __init__(
43
+            self,
44
+            user: typing.Optional[JitsiMeetUser]=None,
45
+            callee: typing.Optional[JitsiMeetUser]=None,
46
+            group: str="default",
47
+    ):
48
+        """
49
+        Context as in JitsiToken Payload
50
+        see https://github.com/jitsi/lib-jitsi-meet/blob/52eb3decf6542413c739ce2209456fac728a89d5/doc/tokens.md  # nopep8
51
+        :param user: Current user
52
+        :param callee: User Who respond in 1-to-1 conf
53
+        :param group: Used only for stats
54
+        """
55
+        self.user = user
56
+        self.callee = callee
57
+        self.group = group
58
+
59
+    def as_dict(self) -> dict:
60
+        data = {}
61
+        if self.callee:
62
+            data['callee'] = self.callee.as_dict()
63
+        if self.user:
64
+            data['user'] = self.user.as_dict()
65
+        if self.group:
66
+            data['group'] = self.group
67
+        return data
68
+
69
+
70
+class JitsiMeetToken(object):
71
+
72
+    def __init__(
73
+            self,
74
+            domain: str,
75
+            room: str,
76
+            app_id: str,
77
+            secret: str,
78
+            alg: str,
79
+            duration: int,
80
+            context: typing.Optional[JitsiMeetContext] = None,
81
+    )-> None:
82
+        """
83
+        JWT token generator for JitsiMeet,
84
+        :param app_id: application identifier
85
+        :param secret: secret share between token generator and XMPP server
86
+        :param alg: algorithm used
87
+        :param duration: duration of token
88
+        :param domain: jitsi-meet domain
89
+        :param room: room name
90
+        """
91
+        self.room = room
92
+        self.domain = domain
93
+        self.app_id = app_id
94
+        self.secret = secret
95
+        self.alg = alg
96
+        self.duration = duration
97
+        self.context = context
98
+
99
+    def generate(self) -> str:
100
+        now = datetime.datetime.utcnow()
101
+        exp = now+datetime.timedelta(seconds=self.duration)
102
+        data = {
103
+            "iss": self.app_id,  # Issuer
104
+            "room": self.room,  # Custom-param for jitsi_meet
105
+            "aud": "*",  # TODO: Understood this param
106
+            "exp": exp,  # Expiration date
107
+            "nbf": now,  # NotBefore
108
+            "iat": now,   # IssuedAt
109
+        }
110
+        if self.context:
111
+            data['context'] = self.context.as_dict()
112
+        jwt_token = jwt.encode(
113
+            data,
114
+            self.secret,
115
+            algorithm=self.alg
116
+        )
117
+        return jwt_token.decode("utf-8")

+ 1 - 1
tracim/tracim/templates/videoconf/invite.mak View File

11
         <p>
11
         <p>
12
         ${_('To invite someone from outside of tracim into this conference, share this link. This link is available for 5 minutes.')}
12
         ${_('To invite someone from outside of tracim into this conference, share this link. This link is available for 5 minutes.')}
13
         </p>
13
         </p>
14
-        <textarea readonly=readonly wrap="off" style="width: 100%;">${jitsi_meet_room.generate_url()}</textarea>
14
+        <textarea readonly=readonly wrap="off" style="width: 100%;">${jitsi_meet_room.generate_url(jitsi_meet_room.generate_token())}</textarea>
15
     </div>
15
     </div>
16
     <div class="modal-footer">
16
     <div class="modal-footer">
17
         <button type="button" class="btn btn-default" data-dismiss="modal">${_('Close')}</button>
17
         <button type="button" class="btn btn-default" data-dismiss="modal">${_('Close')}</button>

+ 10 - 5
tracim/tracim/templates/videoconf/jitsi_meet.mak View File

46
         var options = {
46
         var options = {
47
 	    // jitsi-meet support now(10-2017) only one way to auto-auth, token,
47
 	    // jitsi-meet support now(10-2017) only one way to auto-auth, token,
48
 	    // which is anonymous BOSH auth with specific url (with token value in params of the url).
48
 	    // which is anonymous BOSH auth with specific url (with token value in params of the url).
49
-            %if jitsi_meet_room.token_config:
49
+            %if jitsi_meet_room.use_token:
50
                 jwt: '${jitsi_meet_room.generate_token()}',
50
                 jwt: '${jitsi_meet_room.generate_token()}',
51
             %endif
51
             %endif
52
             roomName : '${jitsi_meet_room.room}',
52
             roomName : '${jitsi_meet_room.room}',
87
         // when others clients use resource part of the Jabber id to do it.
87
         // when others clients use resource part of the Jabber id to do it.
88
         // That's why displayName compat with others XMPP client is not optimal.
88
         // That's why displayName compat with others XMPP client is not optimal.
89
         // check this : https://github.com/jitsi/jitsi-meet/pull/2068
89
         // check this : https://github.com/jitsi/jitsi-meet/pull/2068
90
-        api.executeCommand('displayName', '${fake_api.current_user.name}');
91
-        // We can override also avatar.
92
-        api.executeCommand('avatarUrl', 'https://avatars0.githubusercontent.com/u/3671647');
90
+        % if jitsi_meet_room.context and jitsi_meet_room.context.user:
91
+            %if jitsi_meet_room.context.user.name:
92
+            api.executeCommand('displayName', '${jitsi_meet_room.context.user.name}');
93
+            %endif
94
+            // We can override also avatar.
95
+            %if jitsi_meet_room.context.user.avatar_url:
96
+            api.executeCommand('avatarUrl', '${jitsi_meet_room.context.user.avatar_url}');
97
+            %endif
98
+        % endif
93
     </script>
99
     </script>
94
 </div>
100
 </div>
95
-
96
 <div id="videoconf-invite-modal-dialog" class="modal bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
101
 <div id="videoconf-invite-modal-dialog" class="modal bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
97
   <div class="modal-dialog">
102
   <div class="modal-dialog">
98
     <div class="modal-content">
103
     <div class="modal-content">

+ 0 - 78
tracim/tracim/tests/library/test_jitsi_meet.py View File

1
-import datetime
2
-
3
-import jwt
4
-from nose.tools import raises
5
-
6
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiTokenConfig
7
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiMeetRoom
8
-from tracim.lib.jitsi_meet.jitsi_meet import NoTokenConfigError
9
-
10
-
11
-class TestJitsiMeetRoom(object):
12
-
13
-    TOKEN_CONFIG = JitsiTokenConfig(
14
-        app_id='test',
15
-        secret='secret',
16
-        alg='HS256',
17
-        duration=60,
18
-    )
19
-    ROOM = 'room'
20
-    DOMAIN = 'tracim'
21
-
22
-    def test_unit__generate_token(self) -> None:
23
-
24
-        jmr = JitsiMeetRoom(
25
-            domain=self.DOMAIN,
26
-            room=self.ROOM,
27
-            token_config=self.TOKEN_CONFIG,
28
-        )
29
-        str_token = jmr.generate_token()
30
-        decoded_token = jwt.decode(str_token,
31
-                                   key=self.TOKEN_CONFIG.secret,
32
-                                   algorithm=self.TOKEN_CONFIG.alg,
33
-                                   audience='*',
34
-                                   issuer=self.TOKEN_CONFIG.app_id,
35
-                                   )
36
-        assert decoded_token.get('room') == self.ROOM
37
-        assert decoded_token.get('iat')
38
-        assert decoded_token.get('iat') == decoded_token.get('nbf')
39
-        assert decoded_token.get('exp') == decoded_token.get('iat') + self.TOKEN_CONFIG.duration  # nopep8
40
-
41
-    @raises(NoTokenConfigError)
42
-    def test_unit__generate_token__no_token(self) -> None:
43
-        jmr = JitsiMeetRoom(
44
-            domain=self.DOMAIN,
45
-            room=self.ROOM,
46
-            token_config=None,
47
-        )
48
-        jmr.generate_token()
49
-
50
-    def test_unit__generate_url_token(self) -> None:
51
-        jmr = JitsiMeetRoom(
52
-            domain=self.DOMAIN,
53
-            room=self.ROOM,
54
-            token_config=self.TOKEN_CONFIG,
55
-        )
56
-
57
-        def generate_token():
58
-            return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNzA3NzksImlzcyI6InRlc3QiLCJhdWQiOiIqIiwibmJmIjoxNTE2MjcwNzE5LCJyb29tIjoicm9vbSIsImlhdCI6MTUxNjI3MDcxOX0.zqFBUcPGjlCfXTjuFP7brqalY8TKlgcg6DUE72KhCx0'  # nopep8
59
-
60
-        jmr.generate_token = generate_token
61
-        url = jmr.generate_url()
62
-        assert url == 'https://{domain}/{room}?jwt={token}'.format(
63
-            domain=self.DOMAIN,
64
-            room=self.ROOM,
65
-            token=generate_token()
66
-        )
67
-
68
-    def test_unit__generate_url_no_token(self) -> None:
69
-        jmr = JitsiMeetRoom(
70
-            domain=self.DOMAIN,
71
-            room=self.ROOM,
72
-            token_config=None,
73
-        )
74
-        url = jmr.generate_url()
75
-        assert url == 'https://{domain}/{room}'.format(
76
-            domain=self.DOMAIN,
77
-            room=self.ROOM,
78
-        )

+ 39 - 0
tracim/tracim/tests/library/test_jitsi_meet_token.py View File

1
+import jwt
2
+from tracim.lib.jitsi_meet.token import JitsiMeetToken
3
+from tracim.lib.jitsi_meet.token import JitsiMeetContext
4
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
5
+
6
+
7
+class TestJitsiMeetToken(object):
8
+
9
+    TOKEN_APP_ID = 'test'
10
+    TOKEN_SECRET = 'secret'
11
+    TOKEN_ALG = 'HS256'
12
+    TOKEN_DURATION = 60
13
+    TOKEN_USER = JitsiMeetUser(name='john', email='john@doe', jitsi_meet_user_id='0')
14
+    TOKEN_CONTEXT = JitsiMeetContext(user=TOKEN_USER, group='test')
15
+    ROOM = 'room'
16
+    DOMAIN = 'tracim'
17
+
18
+    def test_unit__generate_token(self) -> None:
19
+
20
+        token = JitsiMeetToken(
21
+            domain=self.DOMAIN,
22
+            room=self.ROOM,
23
+            app_id=self.TOKEN_APP_ID,
24
+            secret=self.TOKEN_SECRET,
25
+            alg=self.TOKEN_ALG,
26
+            duration=self.TOKEN_DURATION,
27
+            context=self.TOKEN_CONTEXT,
28
+        )
29
+        str_token = token.generate()
30
+        decoded_token = jwt.decode(str_token,
31
+                                   key=self.TOKEN_SECRET,
32
+                                   algorithm=self.TOKEN_ALG,
33
+                                   audience='*',
34
+                                   issuer=self.TOKEN_APP_ID,
35
+                                   )
36
+        assert decoded_token.get('room') == self.ROOM
37
+        assert decoded_token.get('iat')
38
+        assert decoded_token.get('iat') == decoded_token.get('nbf')
39
+        assert decoded_token.get('exp') == decoded_token.get('iat') + self.TOKEN_DURATION  # nopep8