Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit e449d43

Browse files
authored
Axsuarez/gov support (#278)
* Gov support with unit tests. Coveralls badge fixed to point correct branch. * Added check to env variables for BF Adapter
1 parent e7353b9 commit e449d43

File tree

11 files changed

+600
-35
lines changed

11 files changed

+600
-35
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# Bot Framework SDK v4 for Python (Preview)
66
[![Build status](https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI)](https://fuselabs.visualstudio.com/SDK_v4/_build/latest?definitionId=431)
77
[![roadmap badge](https://img.shields.io/badge/visit%20the-roadmap-blue.svg)](https://github.com/Microsoft/botbuilder-python/wiki/Roadmap)
8-
[![Coverage Status](https://coveralls.io/repos/github/microsoft/botbuilder-python/badge.svg?branch=axsuarez/formatting-and-style)](https://coveralls.io/github/microsoft/botbuilder-python?branch=axsuarez/formatting-and-style)
8+
[![Coverage Status](https://coveralls.io/repos/github/microsoft/botbuilder-python/badge.svg?branch=HEAD)](https://coveralls.io/github/microsoft/botbuilder-python?branch=HEAD)
99
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
1010

1111
This repository contains code for the Python version of the [Microsoft Bot Framework SDK](https://github.com/Microsoft/botbuilder). The Bot Framework SDK v4 enables developers to model conversation and build sophisticated bot applications using Python. This Python version is in **Preview** state and is being actively developed.

libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import asyncio
55
import base64
66
import json
7+
import os
78
from typing import List, Callable, Awaitable, Union, Dict
89
from msrest.serialization import Model
910
from botbuilder.schema import (
@@ -16,6 +17,10 @@
1617
from botframework.connector import Channels, EmulatorApiClient
1718
from botframework.connector.aio import ConnectorClient
1819
from botframework.connector.auth import (
20+
AuthenticationConstants,
21+
ChannelValidation,
22+
GovernmentChannelValidation,
23+
GovernmentConstants,
1924
MicrosoftAppCredentials,
2025
JwtTokenValidation,
2126
SimpleCredentialProvider,
@@ -81,6 +86,13 @@ class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
8186
def __init__(self, settings: BotFrameworkAdapterSettings):
8287
super(BotFrameworkAdapter, self).__init__()
8388
self.settings = settings or BotFrameworkAdapterSettings("", "")
89+
self.settings.channel_service = self.settings.channel_service or os.environ.get(
90+
AuthenticationConstants.CHANNEL_SERVICE
91+
)
92+
self.settings.open_id_metadata = (
93+
self.settings.open_id_metadata
94+
or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)
95+
)
8496
self._credentials = MicrosoftAppCredentials(
8597
self.settings.app_id,
8698
self.settings.app_password,
@@ -91,6 +103,20 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
91103
)
92104
self._is_emulating_oauth_cards = False
93105

106+
if self.settings.open_id_metadata:
107+
ChannelValidation.open_id_metadata_endpoint = self.settings.open_id_metadata
108+
GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT = (
109+
self.settings.open_id_metadata
110+
)
111+
112+
if JwtTokenValidation.is_government(self.settings.channel_service):
113+
self._credentials.oauth_endpoint = (
114+
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
115+
)
116+
self._credentials.oauth_scope = (
117+
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
118+
)
119+
94120
async def continue_conversation(
95121
self, bot_id: str, reference: ConversationReference, callback: Callable
96122
):
@@ -206,7 +232,10 @@ async def authenticate_request(self, request: Activity, auth_header: str):
206232
:return:
207233
"""
208234
claims = await JwtTokenValidation.authenticate_request(
209-
request, auth_header, self._credential_provider
235+
request,
236+
auth_header,
237+
self._credential_provider,
238+
self.settings.channel_service,
210239
)
211240

212241
if not claims.is_authenticated:

libraries/botframework-connector/botframework/connector/auth/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
from .emulator_validation import *
1818
from .jwt_token_extractor import *
1919
from .government_constants import *
20+
from .authentication_constants import *
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
from abc import ABC
4+
5+
6+
class AuthenticationConstants(ABC):
7+
8+
# TO CHANNEL FROM BOT: Login URL
9+
#
10+
# DEPRECATED: DO NOT USE
11+
TO_CHANNEL_FROM_BOT_LOGIN_URL = (
12+
"https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token"
13+
)
14+
15+
# TO CHANNEL FROM BOT: Login URL prefix
16+
TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = "https://login.microsoftonline.com/"
17+
18+
# TO CHANNEL FROM BOT: Login URL token endpoint path
19+
TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH = "/oauth2/v2.0/token"
20+
21+
# TO CHANNEL FROM BOT: Default tenant from which to obtain a token for bot to channel communication
22+
DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com"
23+
24+
# TO CHANNEL FROM BOT: OAuth scope to request
25+
TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = "https://api.botframework.com/.default"
26+
27+
# TO BOT FROM CHANNEL: Token issuer
28+
TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com"
29+
30+
# Application Setting Key for the OpenIdMetadataUrl value.
31+
BOT_OPEN_ID_METADATA_KEY = "BotOpenIdMetadata"
32+
33+
# Application Setting Key for the ChannelService value.
34+
CHANNEL_SERVICE = "ChannelService"
35+
36+
# Application Setting Key for the OAuthUrl value.
37+
OAUTH_URL_KEY = "OAuthApiEndpoint"
38+
39+
# Application Settings Key for whether to emulate OAuthCards when using the emulator.
40+
EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards"
41+
42+
# TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA
43+
TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = (
44+
"https://login.botframework.com/v1/.well-known/openidconfiguration"
45+
)
46+
47+
# TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA
48+
TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT = (
49+
"https://{channelService}.enterprisechannel.botframework.com"
50+
"/v1/.well-known/openidconfiguration"
51+
)
52+
53+
# TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA
54+
TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = (
55+
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"
56+
)
57+
58+
# Allowed token signing algorithms. Tokens come from channels to the bot. The code
59+
# that uses this also supports tokens coming from the emulator.
60+
ALLOWED_SIGNING_ALGORITHMS = ["RS256", "RS384", "RS512"]
61+
62+
# "azp" Claim.
63+
# Authorized party - the party to which the ID Token was issued.
64+
# This claim follows the general format set forth in the OpenID Spec.
65+
# http://openid.net/specs/openid-connect-core-1_0.html#IDToken
66+
AUTHORIZED_PARTY = "azp"
67+
68+
"""
69+
Audience Claim. From RFC 7519.
70+
https://tools.ietf.org/html/rfc7519#section-4.1.3
71+
The "aud" (audience) claim identifies the recipients that the JWT is
72+
intended for. Each principal intended to process the JWT MUST
73+
identify itself with a value in the audience claim.If the principal
74+
processing the claim does not identify itself with a value in the
75+
"aud" claim when this claim is present, then the JWT MUST be
76+
rejected.In the general case, the "aud" value is an array of case-
77+
sensitive strings, each containing a StringOrURI value.In the
78+
special case when the JWT has one audience, the "aud" value MAY be a
79+
single case-sensitive string containing a StringOrURI value.The
80+
interpretation of audience values is generally application specific.
81+
Use of this claim is OPTIONAL.
82+
"""
83+
AUDIENCE_CLAIM = "aud"
84+
85+
"""
86+
Issuer Claim. From RFC 7519.
87+
https://tools.ietf.org/html/rfc7519#section-4.1.1
88+
The "iss" (issuer) claim identifies the principal that issued the
89+
JWT. The processing of this claim is generally application specific.
90+
The "iss" value is a case-sensitive string containing a StringOrURI
91+
value. Use of this claim is OPTIONAL.
92+
"""
93+
ISSUER_CLAIM = "iss"
94+
95+
"""
96+
From RFC 7515
97+
https://tools.ietf.org/html/rfc7515#section-4.1.4
98+
The "kid" (key ID) Header Parameter is a hint indicating which key
99+
was used to secure the JWS. This parameter allows originators to
100+
explicitly signal a change of key to recipients. The structure of
101+
the "kid" value is unspecified. Its value MUST be a case-sensitive
102+
string. Use of this Header Parameter is OPTIONAL.
103+
When used with a JWK, the "kid" value is used to match a JWK "kid"
104+
parameter value.
105+
"""
106+
KEY_ID_HEADER = "kid"
107+
108+
# Token version claim name. As used in Microsoft AAD tokens.
109+
VERSION_CLAIM = "ver"
110+
111+
# App ID claim name. As used in Microsoft AAD 1.0 tokens.
112+
APP_ID_CLAIM = "appid"
113+
114+
# Service URL claim name. As used in Microsoft Bot Framework v3.1 auth.
115+
SERVICE_URL_CLAIM = "serviceurl"

libraries/botframework-connector/botframework/connector/auth/emulator_validation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ class EmulatorValidation:
2525
"https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
2626
# ???
2727
"https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/",
28+
# Auth for US Gov, 1.0 token
29+
"https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/",
30+
# Auth for US Gov, 2.0 token
31+
"https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0",
2832
],
2933
audience=None,
3034
clock_tolerance=5 * 60,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from abc import ABC
5+
6+
from .authentication_constants import AuthenticationConstants
7+
from .channel_validation import ChannelValidation
8+
from .claims_identity import ClaimsIdentity
9+
from .credential_provider import CredentialProvider
10+
from .jwt_token_extractor import JwtTokenExtractor
11+
from .verify_options import VerifyOptions
12+
13+
14+
class EnterpriseChannelValidation(ABC):
15+
16+
TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
17+
issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
18+
audience=None,
19+
clock_tolerance=5 * 60,
20+
ignore_expiration=False,
21+
)
22+
23+
@staticmethod
24+
async def authenticate_channel_token(
25+
auth_header: str,
26+
credentials: CredentialProvider,
27+
channel_id: str,
28+
channel_service: str,
29+
) -> ClaimsIdentity:
30+
endpoint = (
31+
ChannelValidation.open_id_metadata_endpoint
32+
if ChannelValidation.open_id_metadata_endpoint
33+
else AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT.replace(
34+
"{channelService}", channel_service
35+
)
36+
)
37+
token_extractor = JwtTokenExtractor(
38+
EnterpriseChannelValidation.TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
39+
endpoint,
40+
AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
41+
)
42+
43+
identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
44+
auth_header, channel_id
45+
)
46+
return await EnterpriseChannelValidation.validate_identity(
47+
identity, credentials
48+
)
49+
50+
@staticmethod
51+
async def authenticate_channel_token_with_service_url(
52+
auth_header: str,
53+
credentials: CredentialProvider,
54+
service_url: str,
55+
channel_id: str,
56+
channel_service: str,
57+
) -> ClaimsIdentity:
58+
identity: ClaimsIdentity = await EnterpriseChannelValidation.authenticate_channel_token(
59+
auth_header, credentials, channel_id, channel_service
60+
)
61+
62+
service_url_claim: str = identity.get_claim_value(
63+
AuthenticationConstants.SERVICE_URL_CLAIM
64+
)
65+
if service_url_claim != service_url:
66+
raise Exception("Unauthorized. service_url claim do not match.")
67+
68+
return identity
69+
70+
@staticmethod
71+
async def validate_identity(
72+
identity: ClaimsIdentity, credentials: CredentialProvider
73+
) -> ClaimsIdentity:
74+
if identity is None:
75+
# No valid identity. Not Authorized.
76+
raise Exception("Unauthorized. No valid identity.")
77+
78+
if not identity.is_authenticated:
79+
# The token is in some way invalid. Not Authorized.
80+
raise Exception("Unauthorized. Is not authenticated.")
81+
82+
# Now check that the AppID in the claim set matches
83+
# what we're looking for. Note that in a multi-tenant bot, this value
84+
# comes from developer code that may be reaching out to a service, hence the
85+
# Async validation.
86+
87+
# Look for the "aud" claim, but only if issued from the Bot Framework
88+
if (
89+
identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
90+
!= AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
91+
):
92+
# The relevant Audience Claim MUST be present. Not Authorized.
93+
raise Exception("Unauthorized. Issuer claim MUST be present.")
94+
95+
# The AppId from the claim in the token must match the AppId specified by the developer.
96+
# In this case, the token is destined for the app, so we find the app ID in the audience claim.
97+
aud_claim: str = identity.get_claim_value(
98+
AuthenticationConstants.AUDIENCE_CLAIM
99+
)
100+
if not await credentials.is_valid_appid(aud_claim or ""):
101+
# The AppId is not valid or not present. Not Authorized.
102+
raise Exception(
103+
f"Unauthorized. Invalid AppId passed on token: { aud_claim }"
104+
)
105+
106+
return identity

0 commit comments

Comments
 (0)