From c2574b5281b6d48551c22261bd43503dc7ae3cf9 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Wed, 19 Dec 2018 21:07:01 -0800 Subject: [PATCH 1/4] Document all the APIs --- msal/application.py | 114 +++++++++++++++++++++++++++++++++++--------- msal/token_cache.py | 9 ++-- 2 files changed, 97 insertions(+), 26 deletions(-) diff --git a/msal/application.py b/msal/application.py index 31737c26..9e231582 100644 --- a/msal/application.py +++ b/msal/application.py @@ -55,14 +55,41 @@ def __init__( client_credential=None, authority=None, validate_authority=True, token_cache=None, verify=True, proxies=None, timeout=None): - """ - :param client_credential: It can be a string containing client secret, - or an X509 certificate container in this form: + """Create an instance of application. + + :param client_id: Your app has a clinet_id after you register it on AAD. + :param client_credential: + For :class:`PublicClientApplication`, you simply use `None` here. + For :class:`ConfidentialClientApplication`, + it can be a string containing client secret, + or an X509 certificate container in this form:: { "private_key": "...-----BEGIN PRIVATE KEY-----...", "thumbprint": "A1B2C3D4E5F6...", } + + :param str authority: + A URL that identifies a token authority. It should be of the format + https://login.microsoftonline.com/your_tenant + By default, we will use https://login.microsoftonline.com/common + :param bool validate_authority: (optional) Turns authority validation + on or off. This parameter default to true. + :param TokenCache cache: + Sets the token cache used by this ClientApplication instance. + By default, an in-memory cache will be created and used. + :param verify: (optional) + It will be passed to the + `verify parameter in the underlying requests library + `_ + :param proxies: (optional) + It will be passed to the + `proxies parameter in the underlying requests library + `_ + :param timeout: (optional) + It will be passed to the + `timeout parameter in the underlying requests library + `_ """ self.client_id = client_id self.client_credential = client_credential @@ -123,13 +150,14 @@ def get_authorization_request_url( **kwargs): """Constructs a URL for you to start a Authorization Code Grant. - :param scopes: + :param list[str] scopes: (Required) Scopes requested to access a protected API (a resource). :param str state: Recommended by OAuth2 for CSRF protection. - :param login_hint: + :param str login_hint: Identifier of the user. Generally a User Principal Name (UPN). - :param redirect_uri: + :param str redirect_uri: Address to return to upon receiving a response from the authority. + :return: The authorization url as a string. """ """ # TBD: this would only be meaningful in a new acquire_token_interactive() :param additional_scope: Additional scope is a concept only in AAD. @@ -161,7 +189,8 @@ def acquire_token_by_authorization_code( """The second half of the Authorization Code Grant. :param code: The authorization code returned from Authorization Server. - :param scopes: + :param list[str] scopes: (Required) + Scopes requested to access a protected API (a resource). If you requested user consent for multiple resources, here you will typically want to provide a subset of what you required in AuthCode. @@ -175,6 +204,11 @@ def acquire_token_by_authorization_code( recipient, called audience. So the developer need to specify a scope so that we can restrict the token to be issued for the corresponding audience. + + :return: A dict representing the json response from AAD: + + - A successful response would contain "access_token" key, + - an error response would contain "error" and usually "error_description". """ # If scope is absent on the wire, STS will give you a token associated # to the FIRST scope sent during the authorization request. @@ -190,13 +224,15 @@ def acquire_token_by_authorization_code( def get_accounts(self, username=None): """Get a list of accounts which previously signed in, i.e. exists in cache. - An account can later be used in acquire_token_silent() to find its tokens. - Each account is a dict. For now, we only document its "username" field. - Your app can choose to display those information to end user, - and allow them to choose one of them to proceed. + An account can later be used in :func:`~acquire_token_silent` + to find its tokens. :param username: Filter accounts with this username only. Case insensitive. + :return: A list of account objects. + Each account is a dict. For now, we only document its "username" field. + Your app can choose to display those information to end user, + and allow user to choose one of his/her accounts to proceed. """ # The following implementation finds accounts only from saved accounts, # but does NOT correlate them with saved RTs. It probably won't matter, @@ -224,15 +260,17 @@ def acquire_token_silent( or by finding a valid refresh token from cache and then automatically use it to redeem a new access token. - The return value will be an new or cached access token, or None. - - :param scopes: Scopes, represented as a list of strings + :param list[str] scopes: (Required) + Scopes requested to access a protected API (a resource). :param account: - one of the account object returned by get_accounts(), + one of the account object returned by :func:`~get_accounts`, or use None when you want to find an access token for this client. :param force_refresh: If True, it will skip Access Token look-up, and try to find a Refresh Token to obtain a new Access Token. + :return: + - A dict containing "access_token" key, when cache lookup succeeds. + - None when cache lookup does not yield anything. """ assert isinstance(scopes, list), "Invalid parameter type" the_authority = Authority(authority) if authority else self.authority @@ -286,6 +324,13 @@ def __init__(self, client_id, client_credential=None, **kwargs): client_id, client_credential=None, **kwargs) def initiate_device_flow(self, scopes=None, **kwargs): + """Initiate a Device Flow instance, + which will be used in :func:`~acquire_token_by_device_flow`. + + :param list[str] scopes: + Scopes requested to access a protected API (a resource). + :return: A dict representing a newly created Device Flow object. + """ return self.client.initiate_device_flow( scope=decorate_scope(scopes or [], self.client_id), **kwargs) @@ -293,11 +338,16 @@ def initiate_device_flow(self, scopes=None, **kwargs): def acquire_token_by_device_flow(self, flow, **kwargs): """Obtain token by a device flow object, with customizable polling effect. - Args: - flow (dict): - A dict previously generated by initiate_device_flow(...). - You can exit the polling loop early, by changing the value of - its "expires_at" key to 0, at any time. + :param dict flow: + A dict previously generated by :func:`~initiate_device_flow`. + By default, this method's polling effect will block current thread. + You can abort the polling loop at any time, + by changing the value of the flow's "expires_at" key to 0. + + :return: A dict representing the json response from AAD: + + - A successful response would contain "access_token" key, + - an error response would contain "error" and usually "error_description". """ return self.client.obtain_token_by_device_flow( flow, @@ -308,7 +358,18 @@ def acquire_token_by_device_flow(self, flow, **kwargs): def acquire_token_by_username_password( self, username, password, scopes=None, **kwargs): - """Gets a token for a given resource via user credentails.""" + """Gets a token for a given resource via user credentails. + + :param str username: Typically a UPN in the form of an email address. + :param str password: The password. + :param list[str] scopes: + Scopes requested to access a protected API (a resource). + + :return: A dict representing the json response from AAD: + + - A successful response would contain "access_token" key, + - an error response would contain "error" and usually "error_description". + """ scopes = decorate_scope(scopes, self.client_id) if not self.authority.is_adfs: user_realm_result = self.authority.user_realm_discovery(username) @@ -348,7 +409,16 @@ def _acquire_token_by_username_password_federated( class ConfidentialClientApplication(ClientApplication): # server-side web app def acquire_token_for_client(self, scopes, **kwargs): - """Acquires token from the service for the confidential client.""" + """Acquires token from the service for the confidential client. + + :param list[str] scopes: (Required) + Scopes requested to access a protected API (a resource). + + :return: A dict representing the json response from AAD: + + - A successful response would contain "access_token" key, + - an error response would contain "error" and usually "error_description". + """ # TBD: force_refresh behavior return self.client.obtain_token_for_client( scope=scopes, # This grant flow requires no scope decoration diff --git a/msal/token_cache.py b/msal/token_cache.py index 7adcd333..2f6166ed 100644 --- a/msal/token_cache.py +++ b/msal/token_cache.py @@ -18,9 +18,9 @@ def base64decode(raw): # This can handle a padding-less raw input class TokenCache(object): """This is considered as a base class containing minimal cache behavior. - Although this class already maintains tokens using unified schema, - it does not serialize/persist them. See subclass SerializableTokenCache - for more details. + Although it maintains tokens using unified schema across all MSAL libraries, + this class does not serialize/persist them. + See subclass :class:`SerializableTokenCache` for details on serialization. """ class CredentialType: @@ -169,7 +169,8 @@ class SerializableTokenCache(TokenCache): """This serialization can be a starting point to implement your own persistence. This class does NOT actually persist the cache on disk/db/etc.. - Depends on your need, the following file-based persistence may be sufficient: + Depending on your need, + the following simple recipe for file-based persistence may be sufficient:: import atexit cache = SerializableTokenCache() From 8a264855a0a1da4607a9b0c0403aa2600b4663eb Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Thu, 20 Dec 2018 12:05:31 -0800 Subject: [PATCH 2/4] Use msal.__version__ in doc --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9001352f..d0a02003 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,9 +24,9 @@ author = u'Microsoft' # The short X.Y version -version = u'' +from msal import __version__ as version # The full version, including alpha/beta/rc tags -release = u'0.1.0' +release = version # -- General configuration --------------------------------------------------- @@ -176,4 +176,4 @@ epub_exclude_files = ['search.html'] -# -- Extension configuration ------------------------------------------------- \ No newline at end of file +# -- Extension configuration ------------------------------------------------- From ad28a6ac1982be8c3197b17015e79e92b82f61e5 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Thu, 20 Dec 2018 13:39:35 -0800 Subject: [PATCH 3/4] Also documenting an info var --- msal/token_cache.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/msal/token_cache.py b/msal/token_cache.py index 2f6166ed..e8f7939d 100644 --- a/msal/token_cache.py +++ b/msal/token_cache.py @@ -182,6 +182,10 @@ class SerializableTokenCache(TokenCache): ) app = ClientApplication(..., token_cache=cache) ... + + :var bool has_state_changed: + Indicates whether the cache state has changed since last + :func:`~serialize` or :func:`~deserialize` call. """ def add(self, event): super(SerializableTokenCache, self).add(event) From 7fef8f99b4e7a650f41baee78daf46484dff79e6 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Fri, 21 Dec 2018 15:44:45 -0800 Subject: [PATCH 4/4] Document the possible failure in Device Flow --- msal/application.py | 3 +++ tests/test_application.py | 1 + 2 files changed, 4 insertions(+) diff --git a/msal/application.py b/msal/application.py index 9e231582..7ddadde1 100644 --- a/msal/application.py +++ b/msal/application.py @@ -330,6 +330,9 @@ def initiate_device_flow(self, scopes=None, **kwargs): :param list[str] scopes: Scopes requested to access a protected API (a resource). :return: A dict representing a newly created Device Flow object. + + - A successful response would contain "user_code" key, among others + - an error response would contain some other readable key/value pairs. """ return self.client.initiate_device_flow( scope=decorate_scope(scopes or [], self.client_id), diff --git a/tests/test_application.py b/tests/test_application.py index b9ca02c8..180bef50 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -109,6 +109,7 @@ def test_device_flow(self): self.app = PublicClientApplication( CONFIG["client_id"], authority=CONFIG["authority"]) flow = self.app.initiate_device_flow(scopes=CONFIG.get("scope")) + assert "user_code" in flow, str(flow) # Provision or policy might block DF logging.warn(flow["message"]) duration = 30