Msg.Auth (msg v0.3.8)
OAuth helper functions for Microsoft Identity Platform.
Provides utilities for the OAuth authorization code flow, enabling delegated permissions (user-context authentication) in addition to the client credentials flow (application-only authentication).
OAuth Flows
Client Credentials (Application-Only)
See Msg.Client.new/1 for application-only authentication using
app permissions like User.ReadWrite.All, Group.ReadWrite.All.
Authorization Code (Delegated Permissions)
Use the functions in this module for delegated permissions like
Calendars.ReadWrite.Shared which require user context:
- Generate authorization URL
- User signs in and grants consent
- Exchange authorization code for tokens
- Use refresh token to get new access tokens
Example
# Step 1: Generate authorization URL
credentials = %{
client_id: "app-id",
tenant_id: "tenant-id"
}
url = Msg.Auth.get_authorization_url(credentials,
redirect_uri: "https://myapp.com/auth/callback",
scopes: ["Calendars.ReadWrite.Shared", "offline_access"],
state: "csrf-token"
)
# Step 2: Redirect user to URL, they sign in and approve
# Microsoft redirects back to: https://myapp.com/auth/callback?code=...&state=...
# Step 3: Exchange code for tokens
credentials = %{
client_id: "app-id",
client_secret: "secret",
tenant_id: "tenant-id"
}
{:ok, tokens} = Msg.Auth.exchange_code_for_tokens(
"authorization-code-here",
credentials,
redirect_uri: "https://myapp.com/auth/callback"
)
# Store tokens.refresh_token securely (encrypted in database)
# Step 4: Use refresh token to get new access tokens
{:ok, new_tokens} = Msg.Auth.refresh_access_token(
tokens.refresh_token,
credentials
)
# Create client with access token
client = Msg.Client.new(new_tokens.access_token)Security Notes
- Always use HTTPS for redirect URIs
- Validate the
stateparameter to prevent CSRF attacks - Store refresh tokens encrypted in your database
- Never log or commit tokens to version control
- Handle token rotation (Microsoft may return new refresh tokens)
References
Summary
Functions
Exchanges an authorization code for access and refresh tokens.
Gets an access token using client credentials (application-only) flow.
Generates the Microsoft OAuth authorization URL for user sign-in.
Gets tokens using Resource Owner Password Credentials (ROPC) flow.
Refreshes an access token using a refresh token.
Types
Functions
@spec exchange_code_for_tokens(String.t(), credentials(), keyword()) :: {:ok, token_response()} | {:error, OAuth2.Response.t() | term()}
Exchanges an authorization code for access and refresh tokens.
After the user signs in and Microsoft redirects to your callback URL with an authorization code, call this function to exchange the code for tokens.
Parameters
code- Authorization code from Microsoft's callbackcredentials- Map with:client_id,:client_secret, and:tenant_idopts- Keyword list of options::redirect_uri(required) - Must match the URI used in authorization request
Returns
{:ok, token_response}- Map with access_token, refresh_token, expires_in, etc.{:error, error}- OAuth error response
Examples
{:ok, tokens} = Msg.Auth.exchange_code_for_tokens(
"authorization-code-from-callback",
%{client_id: "app-id", client_secret: "secret", tenant_id: "tenant-id"},
redirect_uri: "https://myapp.com/auth/callback"
)
# Store tokens.refresh_token securely (encrypted!)
# Use tokens.access_token to create client:
client = Msg.Client.new(tokens.access_token)Error Handling
Common errors:
invalid_grant- Code expired or already used (codes expire in 10 minutes)invalid_client- Invalid client_id or client_secretredirect_uri_mismatch- redirect_uri doesn't match authorization request
@spec get_app_token(credentials()) :: {:ok, %{access_token: String.t(), expires_in: integer(), token_type: String.t()}} | {:error, term()}
Gets an access token using client credentials (application-only) flow.
This is similar to Msg.Client.fetch_token!/1 but returns token metadata
for lifecycle management, making it suitable for TokenManager implementations.
Parameters
credentials- Map with:client_id,:client_secret, and:tenant_id
Returns
{:ok, token_response}- Map with access_token, expires_in, and token_type{:error, error}- OAuth error response
Examples
{:ok, token_info} = Msg.Auth.get_app_token(%{
client_id: "app-id",
client_secret: "secret",
tenant_id: "tenant-id"
})
# Store token with accurate expiry
expires_at = DateTime.add(DateTime.utc_now(), token_info.expires_in, :second)
store_token(token_info.access_token, expires_at)Difference from Msg.Client.fetch_token!/1
- Returns
{:ok, metadata}instead of raising - Includes
expires_infor accurate lifecycle management - Designed for token managers, not immediate client creation
@spec get_authorization_url( credentials_without_secret(), keyword() ) :: String.t()
Generates the Microsoft OAuth authorization URL for user sign-in.
This is the first step in the authorization code flow. Redirect the user
to this URL, where they will sign in and grant permissions. Microsoft will
then redirect back to your redirect_uri with an authorization code.
Parameters
credentials- Map with:client_idand:tenant_id(no secret needed)opts- Keyword list of options::redirect_uri(required) - HTTPS URL where Microsoft redirects after auth:scopes(required) - List of permission scopes to request:state(optional) - Random string for CSRF protection (recommended):prompt(optional) - Controls sign-in behavior. Values:"select_account"- Shows account picker (use different account than current session)"login"- Forces credential entry (no SSO)"consent"- Shows consent dialog after sign-in"none"- No interactive prompt (fails if interaction required)
Returns
Authorization URL string to redirect the user to.
Examples
url = Msg.Auth.get_authorization_url(
%{client_id: "app-id", tenant_id: "tenant-id"},
redirect_uri: "https://myapp.com/auth/callback",
scopes: ["Calendars.ReadWrite.Shared", "Group.ReadWrite.All", "offline_access"],
state: "random-csrf-token"
)
# Redirect user to this URL
# After sign-in, Microsoft redirects to:
# https://myapp.com/auth/callback?code=...&state=random-csrf-tokenImportant
- Always include
"offline_access"scope to receive a refresh token - Validate the
stateparameter in your callback to prevent CSRF attacks - The redirect URI must be registered in your Azure AD app configuration
@spec get_tokens_via_password( credentials(), keyword() ) :: {:ok, token_response()} | {:error, term()}
Gets tokens using Resource Owner Password Credentials (ROPC) flow.
WARNING: This flow is discouraged by Microsoft and should only be used for automated testing. It does not work with:
- Accounts with MFA enabled
- Federated/SSO accounts (e.g., Azure AD B2C)
- Personal Microsoft accounts (only works with Azure AD work/school accounts)
Security Notes:
- Never use in production - use authorization code flow instead
- Test accounts should use strong passwords despite not having MFA
- Rotate test account passwords regularly
- Restrict test account permissions to minimum required
Parameters
credentials- Map with:client_id,:client_secret,:tenant_idopts- Keyword list::username(required) - User's email/UPN:password(required) - User's password:scopes(optional) - List of scopes (defaults to Graph API default scope)
Returns
{:ok, token_response}- Map with access_token, refresh_token, expires_in, etc.{:error, error}- OAuth error response
Examples
# For integration tests only
{:ok, tokens} = Msg.Auth.get_tokens_via_password(
%{client_id: "...", client_secret: "...", tenant_id: "..."},
username: "testuser@contoso.onmicrosoft.com",
password: System.get_env("MICROSOFT_SYSTEM_USER_PASSWORD"),
scopes: ["Calendars.ReadWrite.Shared", "Group.ReadWrite.All", "offline_access"]
)
# Use access token to create client
client = Msg.Client.new(tokens.access_token)Azure AD Setup Requirements
- Create test user in Azure AD (e.g., testuser@yourtenant.onmicrosoft.com)
- Disable MFA for this test user
- In App Registration → Authentication → Advanced settings:
- Set "Allow public client flows" to Yes
- Grant required permissions and admin consent
- Store credentials in .env (never commit to git)
Common Errors
AADSTS50126- Invalid username or passwordAADSTS7000218- Public client flows not enabled (see setup step 3)AADSTS50076- MFA required (ROPC does not support MFA)AADSTS700016- Application not found in tenant
References
@spec refresh_access_token(String.t(), credentials(), keyword()) :: {:ok, token_response()} | {:error, OAuth2.Response.t() | term()}
Refreshes an access token using a refresh token.
Refresh tokens are long-lived and can be used to obtain new access tokens without requiring the user to sign in again. Call this function before the access token expires (typically every hour).
Parameters
refresh_token- Valid refresh token from a previous token exchangecredentials- Map with:client_id,:client_secret, and:tenant_idopts- Keyword list of options::scopes(optional) - List of scopes to request (defaults to original scopes)
Returns
{:ok, token_response}- Map with new access_token, possibly new refresh_token, expires_in, etc.{:error, error}- OAuth error response
Examples
{:ok, new_tokens} = Msg.Auth.refresh_access_token(
stored_refresh_token,
%{client_id: "app-id", client_secret: "secret", tenant_id: "tenant-id"}
)
# Microsoft may return a new refresh token (token rotation)
# Always update your stored refresh token:
if Map.has_key?(new_tokens, :refresh_token) do
update_stored_refresh_token(new_tokens.refresh_token)
end
# Use new access token
client = Msg.Client.new(new_tokens.access_token)Token Rotation
Microsoft may return a new refresh token in the response. Always check for
refresh_token in the response and update your stored token if present.
Error Handling
Common errors:
invalid_grant- Refresh token expired or revoked (requires user to re-authenticate)invalid_client- Invalid client credentials