Skip to content

Commit 0731f78

Browse files
tracyboehreraxelsrz
authored andcommitted
Raising PermissionError for auth failures. (#571)
* Raising PermissionError for auth failures. * Black corrections
1 parent 2f7f2da commit 0731f78

File tree

6 files changed

+792
-784
lines changed

6 files changed

+792
-784
lines changed
Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1-
# Copyright (c) Microsoft Corporation. All rights reserved.
2-
# Licensed under the MIT License.
3-
4-
from aiohttp.web import (
5-
middleware,
6-
HTTPNotImplemented,
7-
HTTPUnauthorized,
8-
HTTPNotFound,
9-
HTTPInternalServerError,
10-
)
11-
12-
from botbuilder.core import BotActionNotImplementedError
13-
14-
15-
@middleware
16-
async def aiohttp_error_middleware(request, handler):
17-
try:
18-
response = await handler(request)
19-
return response
20-
except BotActionNotImplementedError:
21-
raise HTTPNotImplemented()
22-
except PermissionError:
23-
raise HTTPUnauthorized()
24-
except KeyError:
25-
raise HTTPNotFound()
26-
except Exception:
27-
raise HTTPInternalServerError()
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from aiohttp.web import (
5+
middleware,
6+
HTTPNotImplemented,
7+
HTTPUnauthorized,
8+
HTTPNotFound,
9+
HTTPInternalServerError,
10+
)
11+
12+
from botbuilder.core import BotActionNotImplementedError
13+
14+
15+
@middleware
16+
async def aiohttp_error_middleware(request, handler):
17+
try:
18+
response = await handler(request)
19+
return response
20+
except BotActionNotImplementedError:
21+
raise HTTPNotImplemented()
22+
except NotImplementedError:
23+
raise HTTPNotImplemented()
24+
except PermissionError:
25+
raise HTTPUnauthorized()
26+
except KeyError:
27+
raise HTTPNotFound()
28+
except Exception:
29+
raise HTTPInternalServerError()
Lines changed: 140 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,140 @@
1-
import asyncio
2-
3-
from .authentication_configuration import AuthenticationConfiguration
4-
from .verify_options import VerifyOptions
5-
from .authentication_constants import AuthenticationConstants
6-
from .jwt_token_extractor import JwtTokenExtractor
7-
from .claims_identity import ClaimsIdentity
8-
from .credential_provider import CredentialProvider
9-
10-
11-
class ChannelValidation:
12-
open_id_metadata_endpoint: str = None
13-
14-
# This claim is ONLY used in the Channel Validation, and not in the emulator validation
15-
SERVICE_URL_CLAIM = "serviceurl"
16-
17-
#
18-
# TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot
19-
#
20-
TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
21-
issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
22-
# Audience validation takes place manually in code.
23-
audience=None,
24-
clock_tolerance=5 * 60,
25-
ignore_expiration=False,
26-
)
27-
28-
@staticmethod
29-
async def authenticate_channel_token_with_service_url(
30-
auth_header: str,
31-
credentials: CredentialProvider,
32-
service_url: str,
33-
channel_id: str,
34-
auth_configuration: AuthenticationConfiguration = None,
35-
) -> ClaimsIdentity:
36-
""" Validate the incoming Auth Header
37-
38-
Validate the incoming Auth Header as a token sent from the Bot Framework Service.
39-
A token issued by the Bot Framework emulator will FAIL this check.
40-
41-
:param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
42-
:type auth_header: str
43-
:param credentials: The user defined set of valid credentials, such as the AppId.
44-
:type credentials: CredentialProvider
45-
:param service_url: Claim value that must match in the identity.
46-
:type service_url: str
47-
48-
:return: A valid ClaimsIdentity.
49-
:raises Exception:
50-
"""
51-
identity = await ChannelValidation.authenticate_channel_token(
52-
auth_header, credentials, channel_id, auth_configuration
53-
)
54-
55-
service_url_claim = identity.get_claim_value(
56-
ChannelValidation.SERVICE_URL_CLAIM
57-
)
58-
if service_url_claim != service_url:
59-
# Claim must match. Not Authorized.
60-
raise Exception("Unauthorized. service_url claim do not match.")
61-
62-
return identity
63-
64-
@staticmethod
65-
async def authenticate_channel_token(
66-
auth_header: str,
67-
credentials: CredentialProvider,
68-
channel_id: str,
69-
auth_configuration: AuthenticationConfiguration = None,
70-
) -> ClaimsIdentity:
71-
""" Validate the incoming Auth Header
72-
73-
Validate the incoming Auth Header as a token sent from the Bot Framework Service.
74-
A token issued by the Bot Framework emulator will FAIL this check.
75-
76-
:param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
77-
:type auth_header: str
78-
:param credentials: The user defined set of valid credentials, such as the AppId.
79-
:type credentials: CredentialProvider
80-
81-
:return: A valid ClaimsIdentity.
82-
:raises Exception:
83-
"""
84-
auth_configuration = auth_configuration or AuthenticationConfiguration()
85-
metadata_endpoint = (
86-
ChannelValidation.open_id_metadata_endpoint
87-
if ChannelValidation.open_id_metadata_endpoint
88-
else AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL
89-
)
90-
91-
token_extractor = JwtTokenExtractor(
92-
ChannelValidation.TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
93-
metadata_endpoint,
94-
AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
95-
)
96-
97-
identity = await token_extractor.get_identity_from_auth_header(
98-
auth_header, channel_id, auth_configuration.required_endorsements
99-
)
100-
101-
return await ChannelValidation.validate_identity(identity, credentials)
102-
103-
@staticmethod
104-
async def validate_identity(
105-
identity: ClaimsIdentity, credentials: CredentialProvider
106-
) -> ClaimsIdentity:
107-
if not identity:
108-
# No valid identity. Not Authorized.
109-
raise Exception("Unauthorized. No valid identity.")
110-
111-
if not identity.is_authenticated:
112-
# The token is in some way invalid. Not Authorized.
113-
raise Exception("Unauthorized. Is not authenticated")
114-
115-
# Now check that the AppID in the claimset matches
116-
# what we're looking for. Note that in a multi-tenant bot, this value
117-
# comes from developer code that may be reaching out to a service, hence the
118-
# Async validation.
119-
120-
# Look for the "aud" claim, but only if issued from the Bot Framework
121-
if (
122-
identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
123-
!= AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
124-
):
125-
# The relevant Audience Claim MUST be present. Not Authorized.
126-
raise Exception("Unauthorized. Audience Claim MUST be present.")
127-
128-
# The AppId from the claim in the token must match the AppId specified by the developer.
129-
# Note that the Bot Framework uses the Audience claim ("aud") to pass the AppID.
130-
aud_claim = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM)
131-
is_valid_app_id = await asyncio.ensure_future(
132-
credentials.is_valid_appid(aud_claim or "")
133-
)
134-
if not is_valid_app_id:
135-
# The AppId is not valid or not present. Not Authorized.
136-
raise Exception("Unauthorized. Invalid AppId passed on token: ", aud_claim)
137-
138-
return identity
1+
import asyncio
2+
3+
from .authentication_configuration import AuthenticationConfiguration
4+
from .verify_options import VerifyOptions
5+
from .authentication_constants import AuthenticationConstants
6+
from .jwt_token_extractor import JwtTokenExtractor
7+
from .claims_identity import ClaimsIdentity
8+
from .credential_provider import CredentialProvider
9+
10+
11+
class ChannelValidation:
12+
open_id_metadata_endpoint: str = None
13+
14+
# This claim is ONLY used in the Channel Validation, and not in the emulator validation
15+
SERVICE_URL_CLAIM = "serviceurl"
16+
17+
#
18+
# TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot
19+
#
20+
TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
21+
issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
22+
# Audience validation takes place manually in code.
23+
audience=None,
24+
clock_tolerance=5 * 60,
25+
ignore_expiration=False,
26+
)
27+
28+
@staticmethod
29+
async def authenticate_channel_token_with_service_url(
30+
auth_header: str,
31+
credentials: CredentialProvider,
32+
service_url: str,
33+
channel_id: str,
34+
auth_configuration: AuthenticationConfiguration = None,
35+
) -> ClaimsIdentity:
36+
""" Validate the incoming Auth Header
37+
38+
Validate the incoming Auth Header as a token sent from the Bot Framework Service.
39+
A token issued by the Bot Framework emulator will FAIL this check.
40+
41+
:param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
42+
:type auth_header: str
43+
:param credentials: The user defined set of valid credentials, such as the AppId.
44+
:type credentials: CredentialProvider
45+
:param service_url: Claim value that must match in the identity.
46+
:type service_url: str
47+
48+
:return: A valid ClaimsIdentity.
49+
:raises Exception:
50+
"""
51+
identity = await ChannelValidation.authenticate_channel_token(
52+
auth_header, credentials, channel_id, auth_configuration
53+
)
54+
55+
service_url_claim = identity.get_claim_value(
56+
ChannelValidation.SERVICE_URL_CLAIM
57+
)
58+
if service_url_claim != service_url:
59+
# Claim must match. Not Authorized.
60+
raise PermissionError("Unauthorized. service_url claim do not match.")
61+
62+
return identity
63+
64+
@staticmethod
65+
async def authenticate_channel_token(
66+
auth_header: str,
67+
credentials: CredentialProvider,
68+
channel_id: str,
69+
auth_configuration: AuthenticationConfiguration = None,
70+
) -> ClaimsIdentity:
71+
""" Validate the incoming Auth Header
72+
73+
Validate the incoming Auth Header as a token sent from the Bot Framework Service.
74+
A token issued by the Bot Framework emulator will FAIL this check.
75+
76+
:param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
77+
:type auth_header: str
78+
:param credentials: The user defined set of valid credentials, such as the AppId.
79+
:type credentials: CredentialProvider
80+
81+
:return: A valid ClaimsIdentity.
82+
:raises Exception:
83+
"""
84+
auth_configuration = auth_configuration or AuthenticationConfiguration()
85+
metadata_endpoint = (
86+
ChannelValidation.open_id_metadata_endpoint
87+
if ChannelValidation.open_id_metadata_endpoint
88+
else AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL
89+
)
90+
91+
token_extractor = JwtTokenExtractor(
92+
ChannelValidation.TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
93+
metadata_endpoint,
94+
AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
95+
)
96+
97+
identity = await token_extractor.get_identity_from_auth_header(
98+
auth_header, channel_id, auth_configuration.required_endorsements
99+
)
100+
101+
return await ChannelValidation.validate_identity(identity, credentials)
102+
103+
@staticmethod
104+
async def validate_identity(
105+
identity: ClaimsIdentity, credentials: CredentialProvider
106+
) -> ClaimsIdentity:
107+
if not identity:
108+
# No valid identity. Not Authorized.
109+
raise PermissionError("Unauthorized. No valid identity.")
110+
111+
if not identity.is_authenticated:
112+
# The token is in some way invalid. Not Authorized.
113+
raise PermissionError("Unauthorized. Is not authenticated")
114+
115+
# Now check that the AppID in the claimset matches
116+
# what we're looking for. Note that in a multi-tenant bot, this value
117+
# comes from developer code that may be reaching out to a service, hence the
118+
# Async validation.
119+
120+
# Look for the "aud" claim, but only if issued from the Bot Framework
121+
if (
122+
identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
123+
!= AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
124+
):
125+
# The relevant Audience Claim MUST be present. Not Authorized.
126+
raise PermissionError("Unauthorized. Audience Claim MUST be present.")
127+
128+
# The AppId from the claim in the token must match the AppId specified by the developer.
129+
# Note that the Bot Framework uses the Audience claim ("aud") to pass the AppID.
130+
aud_claim = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM)
131+
is_valid_app_id = await asyncio.ensure_future(
132+
credentials.is_valid_appid(aud_claim or "")
133+
)
134+
if not is_valid_app_id:
135+
# The AppId is not valid or not present. Not Authorized.
136+
raise PermissionError(
137+
"Unauthorized. Invalid AppId passed on token: ", aud_claim
138+
)
139+
140+
return identity

0 commit comments

Comments
 (0)