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
6 changes: 3 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------
Expand Down Expand Up @@ -176,4 +176,4 @@
epub_exclude_files = ['search.html']


# -- Extension configuration -------------------------------------------------
# -- Extension configuration -------------------------------------------------
117 changes: 95 additions & 22 deletions msal/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#ssl-cert-verification>`_
:param proxies: (optional)
It will be passed to the
`proxies parameter in the underlying requests library
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#proxies>`_
:param timeout: (optional)
It will be passed to the
`timeout parameter in the underlying requests library
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#timeouts>`_
"""
self.client_id = client_id
self.client_credential = client_credential
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -286,18 +324,33 @@ 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.

- 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),
**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,
Expand All @@ -308,7 +361,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)
Expand Down Expand Up @@ -348,7 +412,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
Expand Down
13 changes: 9 additions & 4 deletions msal/token_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -181,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)
Expand Down
1 change: 1 addition & 0 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down