Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
124 changes: 124 additions & 0 deletions .github/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# AI Coding Assistant Skills for Microsoft.Identity.Web

This folder contains **Skills** - specialized knowledge modules that help AI coding assistants provide better assistance for specific scenarios.

## What Are Skills?

Skills are an **open standard** for sharing domain-specific knowledge with AI coding assistants. They are markdown files with structured guidance that AI assistants use when helping with specific tasks. Unlike general instructions, skills are **scenario-specific** and activated when the assistant detects relevant context (keywords, file patterns, or explicit requests).

### Supported AI Assistants

Skills work with multiple AI coding assistants that support the open skills format:

- **GitHub Copilot** - Native support in VS Code, Visual Studio, GitHub Copilot CLI, and other IDEs
- **Claude** (Anthropic) - Via Claude for VS Code extension and Claude Code
- **Other assistants** - Any AI tool that follows the skills convention

## Available Skills

| Skill | Description | Full Guide |
|-------|-------------|------------|
| [entra-id-aspire-authentication](./entra-id-aspire-authentication/SKILL.md) | Adding Microsoft Entra ID authentication to .NET Aspire applications | [Aspire Integration Guide](../../docs/frameworks/aspire.md) |
| [entra-id-aspire-provisioning](./entra-id-aspire-provisioning/SKILL.md) | Provisioning Entra ID app registrations for Aspire apps using Microsoft Graph PowerShell | [Aspire Integration Guide](../../docs/frameworks/aspire.md) |

> **💡 Tip:** Skills are condensed versions optimized for AI assistants. For comprehensive documentation with detailed explanations, diagrams, and troubleshooting, see the linked full guides.
>
> **🔄 Two-phase workflow:** Use the **authentication skill** first to add code (Phase 1), then the **provisioning skill** to create app registrations (Phase 2).

## How to Use Skills

### Option 1: Repository-Level (Recommended for Teams)

Copy the skill folder to your project's `.github/skills/` directory:

```
your-repo/
├── .github/
│ └── skills/
│ └── entra-id-aspire-authentication/
│ └── SKILL.md
```

Copilot will automatically use this skill when working in your repository.

### Option 2: User-Level (Personal Setup)

Install skills globally so they're available across all your projects:

**Windows:**
```powershell
# Create the skills directory
mkdir "$env:USERPROFILE\.github\skills\entra-id-aspire-authentication" -Force

# Copy the skill (or download from this repo)
Copy-Item "SKILL.md" "$env:USERPROFILE\.github\skills\entra-id-aspire-authentication\"
```

Location: `%USERPROFILE%\.github\skills\`

**Linux / macOS:**
```bash
# Create the skills directory
mkdir -p ~/.github/skills/entra-id-aspire-authentication

# Copy the skill (or download from this repo)
cp SKILL.md ~/.github/skills/entra-id-aspire-authentication/
```

Location: `~/.github/skills/`

### Option 3: Reference in Chat

You can also explicitly tell Copilot to use a skill:

> "Using the entra-id-aspire-authentication skill, add authentication to my Aspire app"

## Skill File Structure

Each skill follows this structure:

```markdown
---
name: skill-name
description: When Copilot should use this skill
license: MIT
---

# Skill Title

## When to Use This Skill
- Trigger condition 1
- Trigger condition 2

## Implementation Guide
...
```

The YAML frontmatter helps AI assistants understand when to apply the skill.

## Creating New Skills

1. Create a folder under `.github/skills/` with your skill name
2. Add a `SKILL.md` file with:
- YAML frontmatter (`name`, `description`, `license`)
- Clear "When to Use" section
- Step-by-step implementation guidance
- Code examples and configuration snippets
- Troubleshooting tips

## Skills vs. Instructions

| Aspect | Instructions file | Skills |
|--------|-------------------|--------|
| Scope | Always active for the repo | Activated by context/keywords |
| Purpose | General coding standards | Specific implementation scenarios |
| Location | `.github/copilot-instructions.md` | `.github/skills/<name>/SKILL.md` |
| Content | Style guides, conventions | Step-by-step tutorials, patterns |
| Standard | Varies by AI assistant | Open standard across assistants |

## Resources

- [Microsoft.Identity.Web Documentation](../../docs/README.md)
- [Aspire Integration Guide](../../docs/frameworks/aspire.md)
- [GitHub copilot skills](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills)
- [GitHub Copilot Documentation](https://docs.github.com/copilot)
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// NOTE: This file is included in Microsoft.Identity.Web package (v3.3.0+).
// This copy is maintained for AI skill reference and documentation purposes.
// For production use, reference the NuGet package directly.

using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;

namespace Microsoft.Identity.Web;

/// <summary>
/// Handles authentication challenges for Blazor Server components.
/// Provides functionality for incremental consent and Conditional Access scenarios.
/// </summary>
public class BlazorAuthenticationChallengeHandler(
NavigationManager navigation,
AuthenticationStateProvider authenticationStateProvider,
IConfiguration configuration)
{
private const string MsaTenantId = "9188040d-6c67-4c5b-b112-36a304b66dad";

/// <summary>
/// Gets the current user's authentication state.
/// </summary>
public async Task<ClaimsPrincipal> GetUserAsync()
{
var authState = await authenticationStateProvider.GetAuthenticationStateAsync();
return authState.User;
}

/// <summary>
/// Checks if the current user is authenticated.
/// </summary>
public async Task<bool> IsAuthenticatedAsync()
{
var user = await GetUserAsync();
return user.Identity?.IsAuthenticated == true;
}

/// <summary>
/// Handles exceptions that may require user re-authentication.
/// Returns true if a challenge was initiated, false otherwise.
/// </summary>
public async Task<bool> HandleExceptionAsync(Exception exception)
{
var challengeException = exception as MicrosoftIdentityWebChallengeUserException
?? exception.InnerException as MicrosoftIdentityWebChallengeUserException;

if (challengeException != null)
{
var user = await GetUserAsync();
ChallengeUser(user, challengeException.Scopes, challengeException.MsalUiRequiredException?.Claims);
return true;
}

return false;
}

/// <summary>
/// Initiates a challenge to authenticate the user or request additional consent.
/// </summary>
public void ChallengeUser(ClaimsPrincipal user, string[]? scopes = null, string? claims = null)
{
var currentUri = navigation.Uri;

// Build scopes string (add OIDC scopes)
var allScopes = (scopes ?? [])
.Union(["openid", "offline_access", "profile"])
.Distinct();
var scopeString = Uri.EscapeDataString(string.Join(" ", allScopes));

// Get login hint from user claims
var loginHint = Uri.EscapeDataString(GetLoginHint(user));

// Get domain hint
var domainHint = Uri.EscapeDataString(GetDomainHint(user));

// Build the challenge URL
var challengeUrl = $"/authentication/login?returnUrl={Uri.EscapeDataString(currentUri)}" +
$"&scope={scopeString}" +
$"&loginHint={loginHint}" +
$"&domainHint={domainHint}";

// Add claims if present (for Conditional Access)
if (!string.IsNullOrEmpty(claims))
{
challengeUrl += $"&claims={Uri.EscapeDataString(claims)}";
}

navigation.NavigateTo(challengeUrl, forceLoad: true);
}

/// <summary>
/// Initiates a challenge with scopes from configuration.
/// </summary>
public async Task ChallengeUserWithConfiguredScopesAsync(string configurationSection)
{
var user = await GetUserAsync();
var scopes = configuration.GetSection(configurationSection).Get<string[]>();
ChallengeUser(user, scopes);
}

private static string GetLoginHint(ClaimsPrincipal user)
{
return user.FindFirst("preferred_username")?.Value ??
user.FindFirst("login_hint")?.Value ??
string.Empty;
}

private static string GetDomainHint(ClaimsPrincipal user)
{
var tenantId = user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value ??
user.FindFirst("tid")?.Value;

if (string.IsNullOrEmpty(tenantId))
return "organizations";

// MSA tenant
if (tenantId == MsaTenantId)
return "consumers";

return "organizations";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// NOTE: This file is included in Microsoft.Identity.Web package (v3.3.0+).
// This copy is maintained for AI skill reference and documentation purposes.
// For production use, reference the NuGet package directly.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace Microsoft.Identity.Web;

/// <summary>
/// Extension methods for mapping login and logout endpoints that support
/// incremental consent and Conditional Access scenarios.
/// </summary>
public static class LoginLogoutEndpointRouteBuilderExtensions
{
/// <summary>
/// Maps login and logout endpoints under the current route group.
/// The login endpoint supports incremental consent via scope, loginHint, domainHint, and claims parameters.
/// </summary>
/// <param name="endpoints">The endpoint route builder.</param>
/// <returns>The endpoint convention builder for further configuration.</returns>
public static IEndpointConventionBuilder MapLoginAndLogout(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("");

// Enhanced login endpoint that supports incremental consent and Conditional Access
group.MapGet("/login", (
string? returnUrl,
string? scope,
string? loginHint,
string? domainHint,
string? claims) =>
{
var properties = GetAuthProperties(returnUrl);

// Add scopes if provided (for incremental consent)
if (!string.IsNullOrEmpty(scope))
{
var scopes = scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);
properties.SetParameter(OpenIdConnectParameterNames.Scope, scopes);
}

// Add login hint (pre-fills username)
if (!string.IsNullOrEmpty(loginHint))
{
properties.SetParameter(OpenIdConnectParameterNames.LoginHint, loginHint);
}

// Add domain hint (skips home realm discovery)
if (!string.IsNullOrEmpty(domainHint))
{
properties.SetParameter(OpenIdConnectParameterNames.DomainHint, domainHint);
}

// Add claims challenge (for Conditional Access / step-up auth)
if (!string.IsNullOrEmpty(claims))
{
properties.Items["claims"] = claims;
}

return TypedResults.Challenge(properties, [OpenIdConnectDefaults.AuthenticationScheme]);
})
.AllowAnonymous();

group.MapPost("/logout", async (HttpContext context) =>
{
string? returnUrl = null;
if (context.Request.HasFormContentType)
{
var form = await context.Request.ReadFormAsync();
returnUrl = form["ReturnUrl"];
}

return TypedResults.SignOut(GetAuthProperties(returnUrl),
[CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]);
})
.DisableAntiforgery();

return group;
}

private static AuthenticationProperties GetAuthProperties(string? returnUrl)
{
const string pathBase = "/";
if (string.IsNullOrEmpty(returnUrl)) returnUrl = pathBase;
else if (returnUrl.StartsWith("//", StringComparison.Ordinal)) returnUrl = pathBase; // Prevent protocol-relative redirects
else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery;
else if (returnUrl[0] != '/') returnUrl = $"{pathBase}{returnUrl}";
return new AuthenticationProperties { RedirectUri = returnUrl };
}
}
Loading