On this page

On March 2, 2026, Microsoft published an advisory on OAuth redirection abuse enabling phishing and malware delivery. Microsoft described phishing-led campaigns where attackers register OAuth apps with attacker-controlled redirect URIs, then send legitimate-looking Microsoft login links that intentionally drive the browser into an authorization error path and bounce victims to attacker infrastructure.

This isn’t credential theft or classic token theft. The user still touches real Microsoft infrastructure, but the attacker wins when Entra ID redirects the browser to the app’s registered URI, which points to a phishing page, malware dropper, or relay endpoint.

This post walks through building detection and hardening for this technique using Microsoft Sentinel and Entra ID Conditional Access.

Hands-on Lab: All KQL queries, PowerShell scripts, and deployment automation are in the companion lab.

Evidence boundary (August 13, 2026): the pinned source revision passed 20 offline contract tests and PowerShell parsing, but this review did not deploy it to a tenant. Portal captures below are historical sandbox evidence from an earlier revision, not proof that the current pinned rules, workbook, or policy were deployed or evaluated live.


How the Attack Works

The OAuth redirect abuse pattern exploits how Entra ID handles authentication errors and consent flows. As documented in RFC 9700 Section 4.11.2 (“Authorization Server as Open Redirector”), attackers can deliberately trigger OAuth errors to force redirects through the authorization server.

OAuth redirect abuse attack flow diagram showing 5 steps: attacker registers a malicious app, sends a crafted OAuth lure, victim authenticates at Microsoft login, Entra returns an OAuth error and redirects to the attacker's URI, and the attacker-controlled landing page takes over
OAuth redirect abuse attack flow โ€” the victim authenticates against legitimate Microsoft infrastructure but lands on an attacker-controlled page after the error redirect.
  1. App Registration โ€” Attacker registers an OAuth app and sets the redirect URI to an attacker-controlled domain (powerappsportals.com, github.io, surge.sh, and gitlab.io were cited by Microsoft)
  2. Phishing Link โ€” Victim receives a link that initiates an OAuth authorization flow with parameters designed to fail at the authorization step, such as prompt=none combined with an invalid or unapproved request
  3. Authorization Error โ€” Entra ID reaches an authorization error state such as interaction_required or access_denied
  4. Error Redirect โ€” Per the OAuth 2.0 spec, Entra ID redirects the victim’s browser to the app’s registered redirect_uri with error parameters appended
  5. Malicious Landing โ€” The victim lands on the attacker’s page, which auto-downloads a ZIP containing LNK files and HTML smuggling loaders, or redirects to an AiTM phishing framework like EvilProxy
  6. Data Exfiltration โ€” The state parameter is repurposed to carry the victim’s email address (encoded via Base64, hex, or custom schemes), so it auto-populates on the phishing page

The key insight: the redirect itself is the win. Microsoft noted the sign-in can fail and still hand the attacker a phishing or malware-delivery opportunity because the browser lands on a malicious page after touching legitimate Microsoft infrastructure.

Why This Works

  • The URL starts with login.microsoftonline.com โ€” it looks legitimate to users and URL filters
  • In the observed Entra flow, prompt=none suppresses the normal consent UI and drives the request down the error path
  • Even security-aware users who would decline consent still get redirected because the error itself triggers the redirect
  • The redirect URI can point to any domain registered in the app โ€” github.io, netlify.app, or free hosting services
  • Microsoft’s advisory confirmed multiple threat actors targeting government and public-sector organizations

Which Error Outcomes Matter Most?

Microsoft’s write-up and the OAuth authorization-code flow docs identify two error outcomes that can appear in this flow:

  • AADSTS65001 / interaction_required โ€” common when silent auth cannot complete because the app or requested permissions do not already have the required consent
  • AADSTS65004 / access_denied โ€” common when a user explicitly declines consent
Error codeCommon meaning in this patternHunt value
AADSTS65001Silent auth fails because prior consent is missing or interaction is requiredInvestigation context; not proof of a redirect
AADSTS65004User explicitly declines consentInvestigation context; not proof of phishing
AADSTS70011Invalid scope or malformed OAuth requestSupporting context only
AADSTS700016App not found in tenantSupporting context only
AADSTS70000Invalid grant or broken authorization flowSupporting context only
AADSTS7000218Missing client assertion / client auth issueSupporting context only

Other OAuth failures such as 70011, 700016, 70000, and 7000218 can still show up while attackers probe or misconfigure the flow, but Microsoft does not document one universal redirect behavior for those numeric codes across every endpoint and flow. Treat them as supporting context, not proof that a browser redirect occurred.

Detection implication: A burst of 65001 or 65004 errors against one unfamiliar app is worth triaging, but SigninLogs does not expose the redirect URL or prove that a browser followed one. Correlate every cluster with app registration, redirect-URI, consent, ownership, and risk context.


Detection Strategy

We need detection at two layers:

  1. Proactive โ€” Find risky OAuth app registrations before they’re weaponized
  2. Reactive โ€” Detect active abuse patterns in sign-in and audit logs

MITRE ATT&CK Mapping

TechniqueIDDetection
Spearphishing LinkT1566.002Rules 1 and 4
Account ManipulationT1098Rule 2

Sentinel Analytics Rules

Four scheduled analytics rules detect the core abuse patterns. Each runs hourly against the last 24 hours of data.

Microsoft Defender portal showing the Analytics page with 4 LAB OAuth redirect abuse detection rules filtered and the detail panel showing the OAuth Consent After Risky Sign-in rule configuration
Historical earlier-revision sandbox capture of four OAuth redirect-abuse rules in Microsoft Sentinel. The detail panel shows the then-deployed configuration; this is not current pinned-revision deployment evidence.

Correlates SigninLogs risk indicators with AuditLogs consent events. If a user’s sign-in session shows phishing risk (unfamiliar features, anonymized IP, malicious IP, suspicious IP, malware-infected IP, or suspicious browser) and they grant OAuth consent within 15 minutes, treat the correlation as a high-priority investigation signal, not proof of abuse.

let PhishingWindow = 15m;
let RiskySignIns = SigninLogs
    | where TimeGenerated > ago(1d)
    | where RiskLevelDuringSignIn in ("high", "medium")
        or RiskEventTypes_V2 has_any ("unfamiliarFeatures", "anonymizedIPAddress", "maliciousIPAddress", "suspiciousIPAddress", "malwareInfectedIPAddress", "suspiciousBrowser")
    | project SignInTime = TimeGenerated, UserPrincipalName, IPAddress, RiskLevelDuringSignIn, RiskEventTypes_V2, CorrelationId;
AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName == "Consent to application"
| extend ConsentInitiatedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend AppDisplayName = tostring(TargetResources[0].displayName)
| extend AppId = tostring(TargetResources[0].id)
| extend ConsentPermissions = tostring(TargetResources[0].modifiedProperties)
| join kind=inner (RiskySignIns) on $left.ConsentInitiatedBy == $right.UserPrincipalName
| where TimeGenerated between (SignInTime .. (SignInTime + PhishingWindow))
| project
    TimeGenerated,
    UserPrincipalName = ConsentInitiatedBy,
    AppDisplayName,
    AppId,
    ConsentPermissions,
    RiskLevel = RiskLevelDuringSignIn,
    RiskEvents = RiskEventTypes_V2,
    SourceIP = IPAddress

Why this matters: Legitimate activity can coincide with a risk-flagged session, and some risk indicators can have benign explanations. A nearby consent event raises the case’s confidence and priority, but analysts should corroborate the app, publisher, permissions, user intent, IP context, and later activity before calling it consent phishing.

Rule 2: Suspicious OAuth Redirect URI Registered

Watches for app registrations or updates that add redirect URIs pointing to free hosting, tunneling services, URL shorteners, or non-HTTPS endpoints. AppAddress normally records objects with an Address member, although legacy or exported events can contain bare strings. The query normalizes both forms, compares oldValue with newValue, and evaluates additions only. It exempts Microsoft’s supported exact localhost and 127.0.0.1 HTTP loopback hosts; other HTTP hosts remain suspicious.

// Match exact hosts or their subdomains after parsing each absolute redirect URI.
let SuspiciousHostRegex = @"^([a-z0-9-]+\.)*(ngrok\.io|ngrok-free\.app|trycloudflare\.com|serveo\.net|localtunnel\.me|workers\.dev|pages\.dev|herokuapp\.com|netlify\.app|vercel\.app|github\.io|gitlab\.io|surge\.sh|glitch\.me|replit\.dev|powerappsportals\.com|webhook\.site|requestbin\.com|pipedream\.com|bit\.ly|tinyurl\.com|t\.co|rebrand\.ly)$";
let ApprovedHttpLoopbackHosts = dynamic(["localhost", "127.0.0.1"]);
AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName in ("Add application", "Update application")
| mv-expand ModifiedProperty = TargetResources[0].modifiedProperties
| where ModifiedProperty.displayName == "AppAddress"
| extend NewAddressItems = parse_json(tostring(ModifiedProperty.newValue)),
    OldAddressItems = parse_json(tostring(ModifiedProperty.oldValue))
| extend NewAddressItems = iff(isnull(NewAddressItems), dynamic([]), NewAddressItems),
    OldAddressItems = iff(isnull(OldAddressItems), dynamic([]), OldAddressItems)
// AppAddress normally stores objects such as {"Address":"https://..."};
// legacy/exported records can contain bare strings. Normalize both shapes.
| extend NewAddressItems = array_concat(NewAddressItems, dynamic([null]))
| mv-apply NewAddressItem = NewAddressItems on (
    extend CandidateNewUri = case(
        gettype(NewAddressItem) == "dictionary", tostring(NewAddressItem.Address),
        gettype(NewAddressItem) == "string", tostring(NewAddressItem),
        "")
    | summarize NewRedirectUris = make_set_if(CandidateNewUri, isnotempty(CandidateNewUri))
)
// Append a null sentinel so an empty oldValue still produces an empty set.
| extend OldAddressItems = array_concat(OldAddressItems, dynamic([null]))
| mv-apply OldAddressItem = OldAddressItems on (
    extend CandidateOldUri = case(
        gettype(OldAddressItem) == "dictionary", tostring(OldAddressItem.Address),
        gettype(OldAddressItem) == "string", tostring(OldAddressItem),
        "")
    | summarize OldRedirectUris = make_set_if(CandidateOldUri, isnotempty(CandidateOldUri))
)
| extend AddedRedirectUris = set_difference(NewRedirectUris, OldRedirectUris)
| mv-expand RedirectUri = AddedRedirectUris to typeof(string)
| extend ParsedRedirectUri = parse_url(RedirectUri)
| extend RedirectScheme = tolower(tostring(ParsedRedirectUri.Scheme)),
    RedirectHost = tolower(tostring(ParsedRedirectUri.Host))
| extend InitiatedByUser = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatedByApp = tostring(InitiatedBy.app.displayName)
| extend AppName = tostring(TargetResources[0].displayName)
| extend AppObjectId = tostring(TargetResources[0].id)
| where RedirectHost matches regex SuspiciousHostRegex
    or (RedirectScheme == "http" and RedirectHost !in~ (ApprovedHttpLoopbackHosts))
| project
    TimeGenerated,
    OperationName,
    AppName,
    AppObjectId,
    NewRedirectUris,
    OldRedirectUris,
    RedirectUri,
    RedirectHost,
    InitiatedByUser,
    InitiatedByApp

Tuning tip: Add your organization’s legitimate development domains to an exclusion list. Developers using ngrok for local testing will generate false positives โ€” but you should know about those too.

Rule 3: OAuth Error Cluster by Application

Groups repeated consent, scope, app-registration, grant, and client-authentication failures by application. Those errors can appear in redirect-abuse investigations, but legitimate consent state and broken application configuration produce them too. The medium-severity rule is a triage lead; it does not prove that a redirect occurred and has no ATT&CK assignment.

// Immutable application IDs only - display names are attacker-controlled and
// Entra does not reserve first-party names, so an attacker can register a
// multi-tenant app called "Microsoft Teams" and be excluded by this rule's own
// allowlist while running the exact flow the rule exists to catch.
//
// Populate from your own tenant rather than from a published list:
//   SigninLogs
//   | where AppDisplayName in ("Microsoft Office", "Azure Portal", "Microsoft Teams", "Outlook Mobile")
//   | summarize SeenAs = make_set(AppDisplayName) by AppId
//
// Left empty deliberately. An unfilled allowlist suppresses nothing, so the rule
// is noisier rather than blind; a wrong GUID pasted in from memory would be worse.
let ApprovedAppIds = dynamic([
    // "00000000-0000-0000-0000-000000000000",  // Microsoft Office
    // "00000000-0000-0000-0000-000000000000",  // Azure Portal
    // "00000000-0000-0000-0000-000000000000",  // Microsoft Teams
    // "00000000-0000-0000-0000-000000000000"   // Outlook Mobile
]);
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType in (
    "65001",   // User or administrator has not consented; interaction is required
    "65004",   // User declined consent
    "70011",   // Invalid scope or other OAuth parameter issue
    "70000",   // Invalid grant; broad authentication-flow failure
    "700016",  // Application not found in tenant
    "7000218", // Request body must contain client_assertion or client_secret
    "AADSTS65001",
    "AADSTS65004",
    "AADSTS70011",
    "AADSTS700016"
)
| extend AppName = AppDisplayName
| extend AppIdUsed = AppId
| where isempty(AppIdUsed) or AppIdUsed !in~ (ApprovedAppIds)
| summarize
    ErrorCount = count(),
    DistinctUsers = dcount(UserPrincipalName),
    Users = make_set(UserPrincipalName, 10),
    ErrorCodes = make_set(ResultType),
    IPs = make_set(IPAddress, 10)
    by AppName, AppIdUsed, bin(TimeGenerated, 1h)
| where ErrorCount > 3 or DistinctUsers > 2
| project
    TimeGenerated,
    AppName,
    AppIdUsed,
    ErrorCount,
    DistinctUsers,
    Users,
    ErrorCodes,
    IPs

Why this remains a triage rule: None of these error codes carries a redirect URI, and each has benign causes. A cluster around one unfamiliar app can help prioritize an investigation only after correlation with redirect-URI changes, consent, app ownership, and risk telemetry.

Why the allowlist uses IDs: Entra application display names are attacker-controlled. Suppressing names such as “Microsoft Teams” would let an attacker hide behind a lookalike name, so the deployed rule and workbook accept only tenant-reviewed immutable AppId values.

When 3+ distinct, nonempty Entra user object IDs consent to the same app within an hour, the cluster becomes a high-priority campaign signal. Repeated events from one account remain visible in the event count but cannot satisfy the threshold. Analysts should still confirm the app, permissions, user intent, and surrounding activity before classifying it as phishing.

let ConsentUserThreshold = 3;
let TimeWindow = 1h;
AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName == "Consent to application"
| extend ConsentUserId = tolower(tostring(InitiatedBy.user.id)),
    ConsentUser = tostring(InitiatedBy.user.userPrincipalName)
| where isnotempty(ConsentUserId)
| extend AppDisplayName = tostring(TargetResources[0].displayName)
| extend AppId = tostring(TargetResources[0].id)
| summarize
    ConsentEventCount = count(),
    DistinctConsentUsers = dcount(ConsentUserId),
    ConsentUserIds = make_set(ConsentUserId, 20),
    ConsentUsers = make_set(ConsentUser, 20),
    FirstConsent = min(TimeGenerated),
    LastConsent = max(TimeGenerated)
    by AppDisplayName, AppId, bin(TimeGenerated, TimeWindow)
| where DistinctConsentUsers >= ConsentUserThreshold
| project
    TimeGenerated,
    AppDisplayName,
    AppId,
    ConsentEventCount,
    DistinctConsentUsers,
    ConsentUserIds,
    ConsentUsers,
    ConsentWindow = LastConsent - FirstConsent

Hunting Queries

Beyond automated detection, five hunting queries support proactive threat hunting:

  1. Review Recent OAuth Grant Events โ€” Summarize consent and grant events retained in AuditLogs over the last 90 days; this is event history, not a current inventory, and it may omit older grants or retain events for grants that were later revoked. Use the Graph audit script for current app and service-principal inventory.
  2. OAuth Sign-ins from Non-Corporate IPs โ€” Find OAuth app authentications from unexpected locations (customize the corporate IP ranges)
  3. Recently Registered Apps with High-Privilege Permissions โ€” Apps created in the last 14 days requesting Mail.Read, Files.ReadWrite.All, Directory.ReadWrite.All, etc.
  4. Recent OAuth Redirect URI Changes โ€” Review redirect URI change events retained in AuditLogs; this is not a complete current registration inventory or an unlimited audit trail
  5. Possible Token Relay After OAuth Redirect Error โ€” Find an OAuth 65001 error followed by a successful token acquisition from a different IP within 30 minutes. This is a triage pattern, not proof of relay; benign network changes are possible, so corroborate the session, app, user, and IP context.

The full KQL for all five hunting queries is in the reviewed companion source. Import them into Sentinel Hunting > Queries to run proactive hunts against your OAuth telemetry.


OAuth Security Workbook

The lab deploys an Azure Workbook that provides a single-pane view of OAuth activity across four panels:

Azure Workbook showing the OAuth Security Dashboard with four panels: Consent Grants Over Time timechart, OAuth Error Patterns by Application table, Recent Redirect URI Changes table, and Top 10 Apps by Consent Count bar chart
Historical earlier-revision OAuth Security Dashboard in a sandbox workspace with minimal test data. It illustrates the four-panel layout, not deployment or query-result evidence for the current pinned workbook.
  • Consent Grants Over Time โ€” Timechart of OAuth consent events by application, showing spikes that indicate bulk consent campaigns
  • OAuth Error Patterns by Application โ€” Table of primary redirect-abuse indicators (65001, 65004) plus related OAuth failures grouped by app and error code
  • Recent Redirect URI Changes โ€” Audit trail of redirect URI modifications across all app registrations
  • Top 10 Apps by Consent Count โ€” Bar chart highlighting apps with the most user consents, surfacing outliers

The workbook uses the same KQL patterns as the analytics rules, giving SOC analysts a dashboard to investigate alerts in context. The time range parameter defaults to 7 days but can be adjusted for broader investigations.


Entra ID Hardening

Detection alone isn’t enough. The lab includes hardening scripts that reduce the attack surface:

The two hardening operations have different Microsoft Graph requirements. The authorization-policy update requires Privileged Role Administrator with Policy.ReadWrite.Authorization; the report-only Conditional Access policy requires Conditional Access Administrator (or Security Administrator) with Policy.Read.All and Policy.ReadWrite.ConditionalAccess.

The most impactful control is to restrict which apps users can consent to. Use the lab’s guarded workflow so the complete original collection is captured for rollback before any tenant-wide change:

./hardening/Set-OAuthHardening.ps1 `
  -ConfirmTenantId "<verified-tenant-guid>" `
  -ExcludedUserIds @("<break-glass-user-object-guid>") `
  -WhatIf

After reviewing the preview, omit -WhatIf to create the report-only CA policy and apply the consent restriction. The script preserves existing managePermissionGrantsForOwnedResource.* entries and assigns managePermissionGrantsForSelf.microsoft-user-default-low. This materially reduces risky third-party app approvals, but it does not revoke existing grants, and redirect-only lures can still succeed if the attacker only needs the browser bounce.

Conditional Access Policy

A CA policy adds step-up authentication to risky OAuth-related sign-ins:

  • Applies to: All users
  • Conditions: Sign-in risk = High or Medium
  • Grant controls: Require MFA
  • Session controls: Sign-in frequency = Every time
Entra admin center showing the LAB - Require MFA for Risky OAuth Sign-ins Conditional Access policy in report-only mode, with All users assigned, sign-in risk conditions configured, MFA grant control, and sign-in frequency set to Every time
Historical earlier-revision report-only Conditional Access capture. It is not current pinned-revision deployment evidence. For any new deployment, supply emergency-access exclusions before policy creation and review measured report-only impact before considering enforcement.

The policy deploys in report-only mode. Actual apply requires both the exact active tenant GUID and at least one reviewed emergency-access object GUID. Before its first Graph mutation, the script persists an owner-only manifest containing the tenant, original and intended consent collections, reviewed exclusions, and intended CA content hash. It atomically records Graph’s exact server-assigned CA ID before changing consent policy.

It verifies the server-created policy’s exact ID and content hash before touching tenant consent, and re-reads the consent collection immediately before and after the guarded update. Owner-only permissions are applied to manifest and temporary Graph request-body files before sensitive content is written. A declined confirmation aborts the operation instead of being mistaken for success.

The script never adopts or updates a CA policy by display name. It fails closed on a same-name foreign policy, changed exclusions, tenant mismatch, uncertain create outcome, or consent/CA drift. Run the report-only policy for 7 days, review the CA insights workbook, and only then consider enforcement. Never rely on adding break-glass exclusions after turning the policy on.

OAuth App Audit

The Audit-OAuthApps.ps1 script enumerates all app registrations and service principals via Microsoft Graph to flag:

  • Apps with redirect URIs pointing to ngrok.io, herokuapp.com, workers.dev, etc.
  • Apps with non-HTTPS redirect URIs (excluding localhost)
  • Apps with high-privilege delegated permissions (Mail.Read, Files.ReadWrite.All, Directory.ReadWrite.All)
  • User-consented permissions (vs admin-consented)
  • Multi-tenant apps registered in your tenant

The audit outputs a CSV with risk scores, sorted by severity. Run it weekly. It reads /oauth2PermissionGrants, so the signed-in operator needs delegated Directory.Read.All and a supported Entra role such as Directory Readers.


Deployment

The entire lab deploys to an existing Microsoft Sentinel workspace:

git clone https://github.com/j-dahl7/oauth-redirect-abuse-sentinel.git
cd oauth-redirect-abuse-sentinel
git checkout 17cda5d320dd4fe1922c310be4e553d086d9109b
# Deploy detection content and run the audit
./scripts/Deploy-Lab.ps1 -ResourceGroup "rg-sentinel-lab" -WorkspaceName "law-sentinel-lab"

# Preview detection + optional hardening (no cloud or local writes)
./scripts/Deploy-Lab.ps1 `
  -ResourceGroup "rg-sentinel-lab" `
  -WorkspaceName "law-sentinel-lab" `
  -ApplyHardening `
  -ConfirmTenantId "<verified-tenant-guid>" `
  -ExcludedUserIds @("<break-glass-user-object-guid>") `
  -WhatIf

# Apply optional tenant hardening in report-only mode
./scripts/Deploy-Lab.ps1 `
  -ResourceGroup "rg-sentinel-lab" `
  -WorkspaceName "law-sentinel-lab" `
  -ApplyHardening `
  -ConfirmTenantId "<verified-tenant-guid>" `
  -ExcludedUserIds @("<break-glass-user-object-guid>")

The script deploys:

  1. 4 Sentinel analytics rules (scheduled, hourly)
  2. 1 Sentinel workbook (OAuth Security Dashboard)
  3. OAuth hardening policies only when -ApplyHardening is present
  4. OAuth app audit report (CSV)

Without -ApplyHardening, deployment remains detection plus read-only audit. -WhatIf reports planned resources and skips the audit CSV, ownership manifest, Graph changes, and temporary request-body files. For rollback, use the pinned script’s -Rollback switch with -ConfirmTenantId; it restores the exact captured consent collection and deletes only the exact manifest-owned CA ID after fresh drift checks immediately before each mutation. A completed rollback rerun is idempotent and performs no additional cloud mutation.

See the full lab documentation for prerequisites, testing steps, and cleanup.


Key Takeaways

  1. OAuth redirect abuse bypasses simple URL filtering โ€” The link starts on login.microsoftonline.com, which looks legitimate to users and many controls.
  2. Error context needs correlation โ€” Microsoft’s example included AADSTS65001, but the same code also occurs in ordinary consent state and does not itself show a redirect.
  3. An OAuth error cluster is a triage lead โ€” Correlate 65001, 65004, and the broader failure set with URI changes, consent events, app ownership, and risk telemetry before escalating.
  4. Restrict user consent now โ€” The low-risk verified-publisher policy meaningfully reduces consent phishing, but you still need to review existing grants.
  5. Deploy CA policies for risky sessions โ€” Step up risky sign-ins before the user reaches the malicious app flow.
  6. Hunt, don’t just detect โ€” The token replay hunting query (Hunt 5) catches attacks that no single-event rule will find

Resources

Jerrad Dahlager

Jerrad Dahlager, CISSP, CCSP

Cloud Security Architect ยท Adjunct Instructor

Marine Corps veteran and firm believer that the best security survives contact with reality.

Have thoughts on this post? I'd love to hear from you.