Skip to content
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Bot Framework SDK v4 for Python (Preview)
[![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)
[![roadmap badge](https://img.shields.io/badge/visit%20the-roadmap-blue.svg)](https://github.com/Microsoft/botbuilder-python/wiki/Roadmap)
[![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)
[![Coverage Status](https://coveralls.io/repos/github/microsoft/botbuilder-python/badge.svg?branch=HEAD)](https://coveralls.io/github/microsoft/botbuilder-python?branch=HEAD)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import asyncio
import base64
import json
import os
from typing import List, Callable, Awaitable, Union, Dict
from msrest.serialization import Model
from botbuilder.schema import (
Expand All @@ -16,6 +17,10 @@
from botframework.connector import Channels, EmulatorApiClient
from botframework.connector.aio import ConnectorClient
from botframework.connector.auth import (
AuthenticationConstants,
ChannelValidation,
GovernmentChannelValidation,
GovernmentConstants,
MicrosoftAppCredentials,
JwtTokenValidation,
SimpleCredentialProvider,
Expand Down Expand Up @@ -81,6 +86,13 @@ class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
def __init__(self, settings: BotFrameworkAdapterSettings):
super(BotFrameworkAdapter, self).__init__()
self.settings = settings or BotFrameworkAdapterSettings("", "")
self.settings.channel_service = self.settings.channel_service or os.environ.get(
AuthenticationConstants.CHANNEL_SERVICE
)
self.settings.open_id_metadata = (
self.settings.open_id_metadata
or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)
)
self._credentials = MicrosoftAppCredentials(
self.settings.app_id,
self.settings.app_password,
Expand All @@ -91,6 +103,20 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
)
self._is_emulating_oauth_cards = False

if self.settings.open_id_metadata:
ChannelValidation.open_id_metadata_endpoint = self.settings.open_id_metadata
GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT = (
self.settings.open_id_metadata
)

if JwtTokenValidation.is_government(self.settings.channel_service):
self._credentials.oauth_endpoint = (
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
)
self._credentials.oauth_scope = (
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
)

async def continue_conversation(
self, bot_id: str, reference: ConversationReference, callback: Callable
):
Expand Down Expand Up @@ -206,7 +232,10 @@ async def authenticate_request(self, request: Activity, auth_header: str):
:return:
"""
claims = await JwtTokenValidation.authenticate_request(
request, auth_header, self._credential_provider
request,
auth_header,
self._credential_provider,
self.settings.channel_service,
)

if not claims.is_authenticated:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
from .emulator_validation import *
from .jwt_token_extractor import *
from .government_constants import *
from .authentication_constants import *
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from abc import ABC


class AuthenticationConstants(ABC):

# TO CHANNEL FROM BOT: Login URL
#
# DEPRECATED: DO NOT USE
TO_CHANNEL_FROM_BOT_LOGIN_URL = (
"https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token"
)

# TO CHANNEL FROM BOT: Login URL prefix
TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = "https://login.microsoftonline.com/"

# TO CHANNEL FROM BOT: Login URL token endpoint path
TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH = "/oauth2/v2.0/token"

# TO CHANNEL FROM BOT: Default tenant from which to obtain a token for bot to channel communication
DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com"

# TO CHANNEL FROM BOT: OAuth scope to request
TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = "https://api.botframework.com/.default"

# TO BOT FROM CHANNEL: Token issuer
TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com"

# Application Setting Key for the OpenIdMetadataUrl value.
BOT_OPEN_ID_METADATA_KEY = "BotOpenIdMetadata"

# Application Setting Key for the ChannelService value.
CHANNEL_SERVICE = "ChannelService"

# Application Setting Key for the OAuthUrl value.
OAUTH_URL_KEY = "OAuthApiEndpoint"

# Application Settings Key for whether to emulate OAuthCards when using the emulator.
EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards"

# TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA
TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = (
"https://login.botframework.com/v1/.well-known/openidconfiguration"
)

# TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA
TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT = (
"https://{channelService}.enterprisechannel.botframework.com"
"/v1/.well-known/openidconfiguration"
)

# TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA
TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = (
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"
)

# Allowed token signing algorithms. Tokens come from channels to the bot. The code
# that uses this also supports tokens coming from the emulator.
ALLOWED_SIGNING_ALGORITHMS = ["RS256", "RS384", "RS512"]

# "azp" Claim.
# Authorized party - the party to which the ID Token was issued.
# This claim follows the general format set forth in the OpenID Spec.
# http://openid.net/specs/openid-connect-core-1_0.html#IDToken
AUTHORIZED_PARTY = "azp"

"""
Audience Claim. From RFC 7519.
https://tools.ietf.org/html/rfc7519#section-4.1.3
The "aud" (audience) claim identifies the recipients that the JWT is
intended for. Each principal intended to process the JWT MUST
identify itself with a value in the audience claim.If the principal
processing the claim does not identify itself with a value in the
"aud" claim when this claim is present, then the JWT MUST be
rejected.In the general case, the "aud" value is an array of case-
sensitive strings, each containing a StringOrURI value.In the
special case when the JWT has one audience, the "aud" value MAY be a
single case-sensitive string containing a StringOrURI value.The
interpretation of audience values is generally application specific.
Use of this claim is OPTIONAL.
"""
AUDIENCE_CLAIM = "aud"

"""
Issuer Claim. From RFC 7519.
https://tools.ietf.org/html/rfc7519#section-4.1.1
The "iss" (issuer) claim identifies the principal that issued the
JWT. The processing of this claim is generally application specific.
The "iss" value is a case-sensitive string containing a StringOrURI
value. Use of this claim is OPTIONAL.
"""
ISSUER_CLAIM = "iss"

"""
From RFC 7515
https://tools.ietf.org/html/rfc7515#section-4.1.4
The "kid" (key ID) Header Parameter is a hint indicating which key
was used to secure the JWS. This parameter allows originators to
explicitly signal a change of key to recipients. The structure of
the "kid" value is unspecified. Its value MUST be a case-sensitive
string. Use of this Header Parameter is OPTIONAL.
When used with a JWK, the "kid" value is used to match a JWK "kid"
parameter value.
"""
KEY_ID_HEADER = "kid"

# Token version claim name. As used in Microsoft AAD tokens.
VERSION_CLAIM = "ver"

# App ID claim name. As used in Microsoft AAD 1.0 tokens.
APP_ID_CLAIM = "appid"

# Service URL claim name. As used in Microsoft Bot Framework v3.1 auth.
SERVICE_URL_CLAIM = "serviceurl"
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class EmulatorValidation:
"https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
# ???
"https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/",
# Auth for US Gov, 1.0 token
"https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/",
# Auth for US Gov, 2.0 token
"https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0",
],
audience=None,
clock_tolerance=5 * 60,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from abc import ABC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?


from .authentication_constants import AuthenticationConstants
from .channel_validation import ChannelValidation
from .claims_identity import ClaimsIdentity
from .credential_provider import CredentialProvider
from .jwt_token_extractor import JwtTokenExtractor
from .verify_options import VerifyOptions


class EnterpriseChannelValidation(ABC):

TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
audience=None,
clock_tolerance=5 * 60,
ignore_expiration=False,
)

@staticmethod
async def authenticate_channel_token(
auth_header: str,
credentials: CredentialProvider,
channel_id: str,
channel_service: str,
) -> ClaimsIdentity:
endpoint = (
ChannelValidation.open_id_metadata_endpoint
if ChannelValidation.open_id_metadata_endpoint
else AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT.replace(
"{channelService}", channel_service
)
)
token_extractor = JwtTokenExtractor(
EnterpriseChannelValidation.TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
endpoint,
AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
)

identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
auth_header, channel_id
)
return await EnterpriseChannelValidation.validate_identity(
identity, credentials
)

@staticmethod
async def authenticate_channel_token_with_service_url(
auth_header: str,
credentials: CredentialProvider,
service_url: str,
channel_id: str,
channel_service: str,
) -> ClaimsIdentity:
identity: ClaimsIdentity = await EnterpriseChannelValidation.authenticate_channel_token(
auth_header, credentials, channel_id, channel_service
)

service_url_claim: str = identity.get_claim_value(
AuthenticationConstants.SERVICE_URL_CLAIM
)
if service_url_claim != service_url:
raise Exception("Unauthorized. service_url claim do not match.")

return identity

@staticmethod
async def validate_identity(
identity: ClaimsIdentity, credentials: CredentialProvider
) -> ClaimsIdentity:
if identity is None:
# No valid identity. Not Authorized.
raise Exception("Unauthorized. No valid identity.")

if not identity.is_authenticated:
# The token is in some way invalid. Not Authorized.
raise Exception("Unauthorized. Is not authenticated.")

# Now check that the AppID in the claim set matches
# what we're looking for. Note that in a multi-tenant bot, this value
# comes from developer code that may be reaching out to a service, hence the
# Async validation.

# Look for the "aud" claim, but only if issued from the Bot Framework
if (
identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
!= AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
):
# The relevant Audience Claim MUST be present. Not Authorized.
raise Exception("Unauthorized. Issuer claim MUST be present.")

# The AppId from the claim in the token must match the AppId specified by the developer.
# In this case, the token is destined for the app, so we find the app ID in the audience claim.
aud_claim: str = identity.get_claim_value(
AuthenticationConstants.AUDIENCE_CLAIM
)
if not await credentials.is_valid_appid(aud_claim or ""):
# The AppId is not valid or not present. Not Authorized.
raise Exception(
f"Unauthorized. Invalid AppId passed on token: { aud_claim }"
)

return identity
Loading