@polymorphism-tech/morph-spec 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -0
- package/bin/morph-spec.js +53 -0
- package/content/.claude/commands/morph-apply.md +66 -0
- package/content/.claude/commands/morph-archive.md +79 -0
- package/content/.claude/commands/morph-costs.md +206 -0
- package/content/.claude/commands/morph-infra.md +209 -0
- package/content/.claude/commands/morph-proposal.md +60 -0
- package/content/.claude/commands/morph-status.md +71 -0
- package/content/.claude/settings.local.json +15 -0
- package/content/.claude/skills/infra/bicep-architect.md +419 -0
- package/content/.claude/skills/infra/container-specialist.md +437 -0
- package/content/.claude/skills/infra/devops-engineer.md +405 -0
- package/content/.claude/skills/integrations/asaas-financial.md +333 -0
- package/content/.claude/skills/integrations/azure-identity.md +309 -0
- package/content/.claude/skills/integrations/clerk-auth.md +290 -0
- package/content/.claude/skills/specialists/azure-architect.md +142 -0
- package/content/.claude/skills/specialists/cost-guardian.md +110 -0
- package/content/.claude/skills/specialists/ef-modeler.md +200 -0
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +245 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +209 -0
- package/content/.claude/skills/specialists/po-pm-advisor.md +197 -0
- package/content/.claude/skills/specialists/standards-architect.md +78 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +325 -0
- package/content/.claude/skills/stacks/dotnet-blazor.md +352 -0
- package/content/.claude/skills/stacks/dotnet-nextjs.md +402 -0
- package/content/.claude/skills/stacks/shopify.md +445 -0
- package/content/.morph/archive/.gitkeep +25 -0
- package/content/.morph/config/agents.json +149 -0
- package/content/.morph/config/config.template.json +96 -0
- package/content/.morph/examples/api-nextjs/README.md +241 -0
- package/content/.morph/examples/api-nextjs/contracts.ts +307 -0
- package/content/.morph/examples/api-nextjs/spec.md +399 -0
- package/content/.morph/examples/api-nextjs/tasks.md +168 -0
- package/content/.morph/examples/micro-saas/README.md +125 -0
- package/content/.morph/examples/micro-saas/contracts.cs +358 -0
- package/content/.morph/examples/micro-saas/decisions.md +246 -0
- package/content/.morph/examples/micro-saas/spec.md +236 -0
- package/content/.morph/examples/micro-saas/tasks.md +150 -0
- package/content/.morph/examples/multi-agent/README.md +309 -0
- package/content/.morph/examples/multi-agent/contracts.cs +433 -0
- package/content/.morph/examples/multi-agent/spec.md +479 -0
- package/content/.morph/examples/multi-agent/tasks.md +185 -0
- package/content/.morph/features/.gitkeep +25 -0
- package/content/.morph/project.md +159 -0
- package/content/.morph/specs/.gitkeep +20 -0
- package/content/.morph/standards/architecture.md +190 -0
- package/content/.morph/standards/azure.md +184 -0
- package/content/.morph/standards/coding.md +342 -0
- package/content/.morph/templates/agent.cs +172 -0
- package/content/.morph/templates/component.razor +239 -0
- package/content/.morph/templates/contracts.cs +217 -0
- package/content/.morph/templates/decisions.md +106 -0
- package/content/.morph/templates/infra/app-insights.bicep +63 -0
- package/content/.morph/templates/infra/container-app-env.bicep +49 -0
- package/content/.morph/templates/infra/container-app.bicep +156 -0
- package/content/.morph/templates/infra/key-vault.bicep +91 -0
- package/content/.morph/templates/infra/main.bicep +155 -0
- package/content/.morph/templates/infra/parameters.dev.json +23 -0
- package/content/.morph/templates/infra/parameters.prod.json +23 -0
- package/content/.morph/templates/infra/sql-database.bicep +103 -0
- package/content/.morph/templates/infra/storage.bicep +106 -0
- package/content/.morph/templates/integrations/asaas-client.cs +387 -0
- package/content/.morph/templates/integrations/asaas-webhook.cs +351 -0
- package/content/.morph/templates/integrations/azure-identity-config.cs +288 -0
- package/content/.morph/templates/integrations/clerk-config.cs +258 -0
- package/content/.morph/templates/job.cs +171 -0
- package/content/.morph/templates/migration.cs +83 -0
- package/content/.morph/templates/proposal.md +155 -0
- package/content/.morph/templates/recap.md +105 -0
- package/content/.morph/templates/repository.cs +141 -0
- package/content/.morph/templates/saas/subscription.cs +347 -0
- package/content/.morph/templates/saas/tenant.cs +338 -0
- package/content/.morph/templates/service.cs +139 -0
- package/content/.morph/templates/spec.md +147 -0
- package/content/.morph/templates/tasks.md +235 -0
- package/content/.morph/templates/test.cs +239 -0
- package/content/CLAUDE.md +318 -0
- package/package.json +50 -0
- package/src/commands/doctor.js +132 -0
- package/src/commands/init.js +121 -0
- package/src/commands/update.js +84 -0
- package/src/utils/file-copier.js +50 -0
- package/src/utils/logger.js +32 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Asaas Financial
|
|
2
|
+
|
|
3
|
+
Especialista em integração com Asaas para pagamentos, cobranças e gestão financeira no Brasil.
|
|
4
|
+
|
|
5
|
+
## Responsabilidades
|
|
6
|
+
|
|
7
|
+
1. **Integrar Asaas API** para pagamentos
|
|
8
|
+
2. **Criar clientes** e cobranças
|
|
9
|
+
3. **Processar webhooks** de eventos
|
|
10
|
+
4. **Gerenciar assinaturas** recorrentes
|
|
11
|
+
|
|
12
|
+
## Triggers
|
|
13
|
+
|
|
14
|
+
Keywords: `asaas`, `payment`, `pix`, `boleto`, `cobranca`, `subscription`, `billing`, `pagamento`
|
|
15
|
+
|
|
16
|
+
## Sobre o Asaas
|
|
17
|
+
|
|
18
|
+
- **Plataforma brasileira** de cobranças e pagamentos
|
|
19
|
+
- **Suporta**: PIX, Boleto, Cartão de Crédito
|
|
20
|
+
- **API REST** (sem SDK .NET oficial)
|
|
21
|
+
- **Webhooks** para notificações em tempo real
|
|
22
|
+
|
|
23
|
+
## Configuração Básica
|
|
24
|
+
|
|
25
|
+
```csharp
|
|
26
|
+
// appsettings.json
|
|
27
|
+
{
|
|
28
|
+
"Asaas": {
|
|
29
|
+
"BaseUrl": "https://sandbox.asaas.com/api/v3",
|
|
30
|
+
"ApiKey": "${ASAAS_API_KEY}"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Services/AsaasOptions.cs
|
|
35
|
+
public class AsaasOptions
|
|
36
|
+
{
|
|
37
|
+
public string BaseUrl { get; set; } = "";
|
|
38
|
+
public string ApiKey { get; set; } = "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Program.cs
|
|
42
|
+
builder.Services.Configure<AsaasOptions>(
|
|
43
|
+
builder.Configuration.GetSection("Asaas"));
|
|
44
|
+
|
|
45
|
+
builder.Services.AddHttpClient<IAsaasClient, AsaasClient>((sp, client) =>
|
|
46
|
+
{
|
|
47
|
+
var options = sp.GetRequiredService<IOptions<AsaasOptions>>().Value;
|
|
48
|
+
client.BaseAddress = new Uri(options.BaseUrl);
|
|
49
|
+
client.DefaultRequestHeaders.Add("access_token", options.ApiKey);
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Cliente HTTP
|
|
54
|
+
|
|
55
|
+
```csharp
|
|
56
|
+
// Services/AsaasClient.cs
|
|
57
|
+
public interface IAsaasClient
|
|
58
|
+
{
|
|
59
|
+
Task<AsaasCustomer> CreateCustomerAsync(CreateCustomerRequest request);
|
|
60
|
+
Task<AsaasPayment> CreatePaymentAsync(CreatePaymentRequest request);
|
|
61
|
+
Task<AsaasPayment> GetPaymentAsync(string paymentId);
|
|
62
|
+
Task<AsaasSubscription> CreateSubscriptionAsync(CreateSubscriptionRequest request);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public class AsaasClient : IAsaasClient
|
|
66
|
+
{
|
|
67
|
+
private readonly HttpClient _httpClient;
|
|
68
|
+
private readonly ILogger<AsaasClient> _logger;
|
|
69
|
+
|
|
70
|
+
public AsaasClient(HttpClient httpClient, ILogger<AsaasClient> logger)
|
|
71
|
+
{
|
|
72
|
+
_httpClient = httpClient;
|
|
73
|
+
_logger = logger;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async Task<AsaasCustomer> CreateCustomerAsync(CreateCustomerRequest request)
|
|
77
|
+
{
|
|
78
|
+
var response = await _httpClient.PostAsJsonAsync("customers", request);
|
|
79
|
+
response.EnsureSuccessStatusCode();
|
|
80
|
+
return await response.Content.ReadFromJsonAsync<AsaasCustomer>()
|
|
81
|
+
?? throw new InvalidOperationException("Failed to deserialize customer");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async Task<AsaasPayment> CreatePaymentAsync(CreatePaymentRequest request)
|
|
85
|
+
{
|
|
86
|
+
_logger.LogInformation("Creating payment for customer {CustomerId}", request.Customer);
|
|
87
|
+
|
|
88
|
+
var response = await _httpClient.PostAsJsonAsync("payments", request);
|
|
89
|
+
|
|
90
|
+
if (!response.IsSuccessStatusCode)
|
|
91
|
+
{
|
|
92
|
+
var error = await response.Content.ReadAsStringAsync();
|
|
93
|
+
_logger.LogError("Failed to create payment: {Error}", error);
|
|
94
|
+
throw new AsaasException($"Failed to create payment: {error}");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return await response.Content.ReadFromJsonAsync<AsaasPayment>()!;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public async Task<AsaasPayment> GetPaymentAsync(string paymentId)
|
|
101
|
+
{
|
|
102
|
+
var response = await _httpClient.GetAsync($"payments/{paymentId}");
|
|
103
|
+
response.EnsureSuccessStatusCode();
|
|
104
|
+
return await response.Content.ReadFromJsonAsync<AsaasPayment>()!;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async Task<AsaasSubscription> CreateSubscriptionAsync(CreateSubscriptionRequest request)
|
|
108
|
+
{
|
|
109
|
+
var response = await _httpClient.PostAsJsonAsync("subscriptions", request);
|
|
110
|
+
response.EnsureSuccessStatusCode();
|
|
111
|
+
return await response.Content.ReadFromJsonAsync<AsaasSubscription>()!;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## DTOs
|
|
117
|
+
|
|
118
|
+
```csharp
|
|
119
|
+
// Models/Asaas/CreateCustomerRequest.cs
|
|
120
|
+
public record CreateCustomerRequest
|
|
121
|
+
{
|
|
122
|
+
[JsonPropertyName("name")]
|
|
123
|
+
public required string Name { get; init; }
|
|
124
|
+
|
|
125
|
+
[JsonPropertyName("cpfCnpj")]
|
|
126
|
+
public required string CpfCnpj { get; init; }
|
|
127
|
+
|
|
128
|
+
[JsonPropertyName("email")]
|
|
129
|
+
public string? Email { get; init; }
|
|
130
|
+
|
|
131
|
+
[JsonPropertyName("phone")]
|
|
132
|
+
public string? Phone { get; init; }
|
|
133
|
+
|
|
134
|
+
[JsonPropertyName("mobilePhone")]
|
|
135
|
+
public string? MobilePhone { get; init; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Models/Asaas/CreatePaymentRequest.cs
|
|
139
|
+
public record CreatePaymentRequest
|
|
140
|
+
{
|
|
141
|
+
[JsonPropertyName("customer")]
|
|
142
|
+
public required string Customer { get; init; } // Asaas customer ID
|
|
143
|
+
|
|
144
|
+
[JsonPropertyName("billingType")]
|
|
145
|
+
public required string BillingType { get; init; } // BOLETO, PIX, CREDIT_CARD
|
|
146
|
+
|
|
147
|
+
[JsonPropertyName("value")]
|
|
148
|
+
public required decimal Value { get; init; }
|
|
149
|
+
|
|
150
|
+
[JsonPropertyName("dueDate")]
|
|
151
|
+
public required string DueDate { get; init; } // yyyy-MM-dd
|
|
152
|
+
|
|
153
|
+
[JsonPropertyName("description")]
|
|
154
|
+
public string? Description { get; init; }
|
|
155
|
+
|
|
156
|
+
[JsonPropertyName("externalReference")]
|
|
157
|
+
public string? ExternalReference { get; init; } // Seu ID interno
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Models/Asaas/AsaasPayment.cs
|
|
161
|
+
public record AsaasPayment
|
|
162
|
+
{
|
|
163
|
+
[JsonPropertyName("id")]
|
|
164
|
+
public string Id { get; init; } = "";
|
|
165
|
+
|
|
166
|
+
[JsonPropertyName("customer")]
|
|
167
|
+
public string Customer { get; init; } = "";
|
|
168
|
+
|
|
169
|
+
[JsonPropertyName("value")]
|
|
170
|
+
public decimal Value { get; init; }
|
|
171
|
+
|
|
172
|
+
[JsonPropertyName("status")]
|
|
173
|
+
public string Status { get; init; } = ""; // PENDING, RECEIVED, CONFIRMED, OVERDUE...
|
|
174
|
+
|
|
175
|
+
[JsonPropertyName("billingType")]
|
|
176
|
+
public string BillingType { get; init; } = "";
|
|
177
|
+
|
|
178
|
+
[JsonPropertyName("invoiceUrl")]
|
|
179
|
+
public string? InvoiceUrl { get; init; }
|
|
180
|
+
|
|
181
|
+
[JsonPropertyName("bankSlipUrl")]
|
|
182
|
+
public string? BankSlipUrl { get; init; } // URL do boleto
|
|
183
|
+
|
|
184
|
+
[JsonPropertyName("pixQrCode")]
|
|
185
|
+
public AsaasPixQrCode? PixQrCode { get; init; }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public record AsaasPixQrCode
|
|
189
|
+
{
|
|
190
|
+
[JsonPropertyName("encodedImage")]
|
|
191
|
+
public string? EncodedImage { get; init; } // Base64 QR Code
|
|
192
|
+
|
|
193
|
+
[JsonPropertyName("payload")]
|
|
194
|
+
public string? Payload { get; init; } // PIX copia-e-cola
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Webhooks
|
|
199
|
+
|
|
200
|
+
```csharp
|
|
201
|
+
// Controllers/AsaasWebhookController.cs
|
|
202
|
+
[ApiController]
|
|
203
|
+
[Route("api/webhooks/asaas")]
|
|
204
|
+
public class AsaasWebhookController : ControllerBase
|
|
205
|
+
{
|
|
206
|
+
private readonly IPaymentService _paymentService;
|
|
207
|
+
private readonly ILogger<AsaasWebhookController> _logger;
|
|
208
|
+
|
|
209
|
+
public AsaasWebhookController(
|
|
210
|
+
IPaymentService paymentService,
|
|
211
|
+
ILogger<AsaasWebhookController> logger)
|
|
212
|
+
{
|
|
213
|
+
_paymentService = paymentService;
|
|
214
|
+
_logger = logger;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
[HttpPost]
|
|
218
|
+
public async Task<IActionResult> HandleWebhook([FromBody] AsaasWebhookPayload payload)
|
|
219
|
+
{
|
|
220
|
+
_logger.LogInformation("Received Asaas webhook: {Event} for payment {PaymentId}",
|
|
221
|
+
payload.Event, payload.Payment?.Id);
|
|
222
|
+
|
|
223
|
+
try
|
|
224
|
+
{
|
|
225
|
+
switch (payload.Event)
|
|
226
|
+
{
|
|
227
|
+
case "PAYMENT_CONFIRMED":
|
|
228
|
+
case "PAYMENT_RECEIVED":
|
|
229
|
+
await _paymentService.ConfirmPaymentAsync(payload.Payment!.Id);
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case "PAYMENT_OVERDUE":
|
|
233
|
+
await _paymentService.MarkOverdueAsync(payload.Payment!.Id);
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case "PAYMENT_REFUNDED":
|
|
237
|
+
await _paymentService.RefundPaymentAsync(payload.Payment!.Id);
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
default:
|
|
241
|
+
_logger.LogWarning("Unhandled webhook event: {Event}", payload.Event);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return Ok();
|
|
246
|
+
}
|
|
247
|
+
catch (Exception ex)
|
|
248
|
+
{
|
|
249
|
+
_logger.LogError(ex, "Error processing webhook");
|
|
250
|
+
return StatusCode(500);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public record AsaasWebhookPayload
|
|
256
|
+
{
|
|
257
|
+
[JsonPropertyName("event")]
|
|
258
|
+
public string Event { get; init; } = "";
|
|
259
|
+
|
|
260
|
+
[JsonPropertyName("payment")]
|
|
261
|
+
public AsaasPayment? Payment { get; init; }
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Assinaturas Recorrentes
|
|
266
|
+
|
|
267
|
+
```csharp
|
|
268
|
+
// Models/Asaas/CreateSubscriptionRequest.cs
|
|
269
|
+
public record CreateSubscriptionRequest
|
|
270
|
+
{
|
|
271
|
+
[JsonPropertyName("customer")]
|
|
272
|
+
public required string Customer { get; init; }
|
|
273
|
+
|
|
274
|
+
[JsonPropertyName("billingType")]
|
|
275
|
+
public required string BillingType { get; init; }
|
|
276
|
+
|
|
277
|
+
[JsonPropertyName("value")]
|
|
278
|
+
public required decimal Value { get; init; }
|
|
279
|
+
|
|
280
|
+
[JsonPropertyName("nextDueDate")]
|
|
281
|
+
public required string NextDueDate { get; init; }
|
|
282
|
+
|
|
283
|
+
[JsonPropertyName("cycle")]
|
|
284
|
+
public required string Cycle { get; init; } // MONTHLY, WEEKLY, BIWEEKLY, YEARLY
|
|
285
|
+
|
|
286
|
+
[JsonPropertyName("description")]
|
|
287
|
+
public string? Description { get; init; }
|
|
288
|
+
|
|
289
|
+
[JsonPropertyName("externalReference")]
|
|
290
|
+
public string? ExternalReference { get; init; }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Criar assinatura
|
|
294
|
+
var subscription = await _asaasClient.CreateSubscriptionAsync(new()
|
|
295
|
+
{
|
|
296
|
+
Customer = customerId,
|
|
297
|
+
BillingType = "PIX",
|
|
298
|
+
Value = 49.90m,
|
|
299
|
+
NextDueDate = DateTime.Today.AddDays(1).ToString("yyyy-MM-dd"),
|
|
300
|
+
Cycle = "MONTHLY",
|
|
301
|
+
Description = "Plano Pro",
|
|
302
|
+
ExternalReference = $"plan_{planId}"
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Ambientes
|
|
307
|
+
|
|
308
|
+
| Ambiente | Base URL |
|
|
309
|
+
|----------|----------|
|
|
310
|
+
| Sandbox | `https://sandbox.asaas.com/api/v3` |
|
|
311
|
+
| Produção | `https://www.asaas.com/api/v3` |
|
|
312
|
+
|
|
313
|
+
## Documentação de Referência
|
|
314
|
+
|
|
315
|
+
- [Asaas API Documentation](https://docs.asaas.com/)
|
|
316
|
+
- [Webhooks](https://docs.asaas.com/reference/webhooks)
|
|
317
|
+
- [Cobranças](https://docs.asaas.com/reference/criar-nova-cobranca)
|
|
318
|
+
- [Assinaturas](https://docs.asaas.com/reference/criar-nova-assinatura)
|
|
319
|
+
|
|
320
|
+
## Checklist de Integração
|
|
321
|
+
|
|
322
|
+
- [ ] API Key configurada (não hardcoded)
|
|
323
|
+
- [ ] HttpClient com retry policy (Polly)
|
|
324
|
+
- [ ] Webhook endpoint configurado
|
|
325
|
+
- [ ] Validação de webhook signature
|
|
326
|
+
- [ ] Logs estruturados
|
|
327
|
+
- [ ] Tratamento de erros Asaas
|
|
328
|
+
- [ ] Testes com sandbox
|
|
329
|
+
- [ ] ExternalReference para reconciliação
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Azure Identity (Microsoft Identity)
|
|
2
|
+
|
|
3
|
+
Especialista em autenticação com Microsoft Identity Platform para aplicações .NET/Blazor.
|
|
4
|
+
|
|
5
|
+
## Responsabilidades
|
|
6
|
+
|
|
7
|
+
1. **Configurar Microsoft Identity** em projetos .NET
|
|
8
|
+
2. **Implementar autenticação** com Azure AD / Entra ID
|
|
9
|
+
3. **Gerenciar tokens** e autorização
|
|
10
|
+
4. **Integrar com APIs** protegidas
|
|
11
|
+
|
|
12
|
+
## Triggers
|
|
13
|
+
|
|
14
|
+
Keywords: `identity`, `entra`, `azure ad`, `microsoft auth`, `msal`, `oauth`, `oidc`, `microsoft identity`
|
|
15
|
+
|
|
16
|
+
## Sobre Microsoft Identity
|
|
17
|
+
|
|
18
|
+
- **Plataforma oficial** da Microsoft para autenticação
|
|
19
|
+
- **Suporta**: Azure AD, Microsoft accounts, B2C
|
|
20
|
+
- **SDK nativo**: Microsoft.Identity.Web
|
|
21
|
+
- **Ideal para**: Enterprise, Azure-first, Microsoft 365
|
|
22
|
+
|
|
23
|
+
## Instalação
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dotnet add package Microsoft.Identity.Web
|
|
27
|
+
dotnet add package Microsoft.Identity.Web.UI # Para Blazor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuração Básica
|
|
31
|
+
|
|
32
|
+
```csharp
|
|
33
|
+
// appsettings.json
|
|
34
|
+
{
|
|
35
|
+
"AzureAd": {
|
|
36
|
+
"Instance": "https://login.microsoftonline.com/",
|
|
37
|
+
"Domain": "yourdomain.onmicrosoft.com",
|
|
38
|
+
"TenantId": "your-tenant-id",
|
|
39
|
+
"ClientId": "your-client-id",
|
|
40
|
+
"ClientSecret": "${AZURE_AD_CLIENT_SECRET}",
|
|
41
|
+
"CallbackPath": "/signin-oidc"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Program.cs
|
|
46
|
+
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
47
|
+
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
|
|
48
|
+
|
|
49
|
+
builder.Services.AddControllersWithViews()
|
|
50
|
+
.AddMicrosoftIdentityUI();
|
|
51
|
+
|
|
52
|
+
builder.Services.AddAuthorization();
|
|
53
|
+
|
|
54
|
+
// Pipeline
|
|
55
|
+
app.UseAuthentication();
|
|
56
|
+
app.UseAuthorization();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Blazor Server
|
|
60
|
+
|
|
61
|
+
```csharp
|
|
62
|
+
// Program.cs
|
|
63
|
+
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
64
|
+
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
|
|
65
|
+
|
|
66
|
+
builder.Services.AddControllersWithViews()
|
|
67
|
+
.AddMicrosoftIdentityUI();
|
|
68
|
+
|
|
69
|
+
builder.Services.AddRazorPages();
|
|
70
|
+
builder.Services.AddServerSideBlazor()
|
|
71
|
+
.AddMicrosoftIdentityConsentHandler();
|
|
72
|
+
|
|
73
|
+
// App.razor
|
|
74
|
+
<CascadingAuthenticationState>
|
|
75
|
+
<Router AppAssembly="@typeof(App).Assembly">
|
|
76
|
+
<Found Context="routeData">
|
|
77
|
+
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
|
78
|
+
<NotAuthorized>
|
|
79
|
+
@if (!context.User.Identity?.IsAuthenticated ?? true)
|
|
80
|
+
{
|
|
81
|
+
<RedirectToLogin />
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
{
|
|
85
|
+
<p>Você não tem permissão para acessar este recurso.</p>
|
|
86
|
+
}
|
|
87
|
+
</NotAuthorized>
|
|
88
|
+
</AuthorizeRouteView>
|
|
89
|
+
</Found>
|
|
90
|
+
</Router>
|
|
91
|
+
</CascadingAuthenticationState>
|
|
92
|
+
|
|
93
|
+
// Components/RedirectToLogin.razor
|
|
94
|
+
@inject NavigationManager Navigation
|
|
95
|
+
|
|
96
|
+
@code {
|
|
97
|
+
protected override void OnInitialized()
|
|
98
|
+
{
|
|
99
|
+
var returnUrl = Uri.EscapeDataString(Navigation.Uri);
|
|
100
|
+
Navigation.NavigateTo($"MicrosoftIdentity/Account/SignIn?redirectUri={returnUrl}", forceLoad: true);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Páginas Protegidas
|
|
106
|
+
|
|
107
|
+
```razor
|
|
108
|
+
@* Pages/Secure.razor *@
|
|
109
|
+
@page "/secure"
|
|
110
|
+
@attribute [Authorize]
|
|
111
|
+
|
|
112
|
+
<h1>Área Protegida</h1>
|
|
113
|
+
|
|
114
|
+
<AuthorizeView>
|
|
115
|
+
<Authorized>
|
|
116
|
+
<p>Bem-vindo, @context.User.Identity?.Name!</p>
|
|
117
|
+
<p>Email: @context.User.FindFirst("preferred_username")?.Value</p>
|
|
118
|
+
</Authorized>
|
|
119
|
+
</AuthorizeView>
|
|
120
|
+
|
|
121
|
+
@* Por role *@
|
|
122
|
+
<AuthorizeView Roles="Admin">
|
|
123
|
+
<Authorized>
|
|
124
|
+
<AdminPanel />
|
|
125
|
+
</Authorized>
|
|
126
|
+
</AuthorizeView>
|
|
127
|
+
|
|
128
|
+
@* Por policy *@
|
|
129
|
+
<AuthorizeView Policy="RequireManagerRole">
|
|
130
|
+
<Authorized>
|
|
131
|
+
<ManagerDashboard />
|
|
132
|
+
</Authorized>
|
|
133
|
+
</AuthorizeView>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Proteger API
|
|
137
|
+
|
|
138
|
+
```csharp
|
|
139
|
+
// Program.cs para API
|
|
140
|
+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
141
|
+
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
|
|
142
|
+
|
|
143
|
+
builder.Services.AddAuthorization();
|
|
144
|
+
|
|
145
|
+
// Controller
|
|
146
|
+
[ApiController]
|
|
147
|
+
[Route("api/[controller]")]
|
|
148
|
+
[Authorize]
|
|
149
|
+
public class ProfileController : ControllerBase
|
|
150
|
+
{
|
|
151
|
+
[HttpGet]
|
|
152
|
+
public IActionResult GetProfile()
|
|
153
|
+
{
|
|
154
|
+
return Ok(new
|
|
155
|
+
{
|
|
156
|
+
UserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
|
|
157
|
+
Name = User.Identity?.Name,
|
|
158
|
+
Email = User.FindFirst("preferred_username")?.Value
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[HttpGet("admin")]
|
|
163
|
+
[Authorize(Roles = "Admin")]
|
|
164
|
+
public IActionResult AdminOnly()
|
|
165
|
+
{
|
|
166
|
+
return Ok("Admin access");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Chamar APIs Protegidas (Downstream APIs)
|
|
172
|
+
|
|
173
|
+
```csharp
|
|
174
|
+
// Program.cs
|
|
175
|
+
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
176
|
+
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
|
|
177
|
+
.EnableTokenAcquisitionToCallDownstreamApi()
|
|
178
|
+
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
|
|
179
|
+
.AddInMemoryTokenCaches();
|
|
180
|
+
|
|
181
|
+
// Service
|
|
182
|
+
public class ProfileService
|
|
183
|
+
{
|
|
184
|
+
private readonly GraphServiceClient _graphClient;
|
|
185
|
+
|
|
186
|
+
public ProfileService(GraphServiceClient graphClient)
|
|
187
|
+
{
|
|
188
|
+
_graphClient = graphClient;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public async Task<User> GetCurrentUserAsync()
|
|
192
|
+
{
|
|
193
|
+
return await _graphClient.Me.GetAsync();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public async Task<byte[]?> GetProfilePhotoAsync()
|
|
197
|
+
{
|
|
198
|
+
try
|
|
199
|
+
{
|
|
200
|
+
var photoStream = await _graphClient.Me.Photo.Content.GetAsync();
|
|
201
|
+
if (photoStream is null) return null;
|
|
202
|
+
|
|
203
|
+
using var memoryStream = new MemoryStream();
|
|
204
|
+
await photoStream.CopyToAsync(memoryStream);
|
|
205
|
+
return memoryStream.ToArray();
|
|
206
|
+
}
|
|
207
|
+
catch
|
|
208
|
+
{
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Authorization Policies
|
|
216
|
+
|
|
217
|
+
```csharp
|
|
218
|
+
// Program.cs
|
|
219
|
+
builder.Services.AddAuthorization(options =>
|
|
220
|
+
{
|
|
221
|
+
options.AddPolicy("RequireAdmin", policy =>
|
|
222
|
+
policy.RequireRole("Admin"));
|
|
223
|
+
|
|
224
|
+
options.AddPolicy("RequireManager", policy =>
|
|
225
|
+
policy.RequireAssertion(context =>
|
|
226
|
+
context.User.IsInRole("Admin") ||
|
|
227
|
+
context.User.IsInRole("Manager")));
|
|
228
|
+
|
|
229
|
+
options.AddPolicy("RequireVerifiedEmail", policy =>
|
|
230
|
+
policy.RequireClaim("email_verified", "true"));
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Multi-tenant
|
|
235
|
+
|
|
236
|
+
```csharp
|
|
237
|
+
// appsettings.json para multi-tenant
|
|
238
|
+
{
|
|
239
|
+
"AzureAd": {
|
|
240
|
+
"Instance": "https://login.microsoftonline.com/",
|
|
241
|
+
"TenantId": "common", // ou "organizations" para apenas work accounts
|
|
242
|
+
"ClientId": "your-client-id",
|
|
243
|
+
"ClientSecret": "${AZURE_AD_CLIENT_SECRET}"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Validar tenant
|
|
248
|
+
builder.Services.Configure<OpenIdConnectOptions>(
|
|
249
|
+
OpenIdConnectDefaults.AuthenticationScheme,
|
|
250
|
+
options =>
|
|
251
|
+
{
|
|
252
|
+
options.TokenValidationParameters.IssuerValidator = (issuer, token, parameters) =>
|
|
253
|
+
{
|
|
254
|
+
// Validar que o tenant é permitido
|
|
255
|
+
var allowedTenants = new[] { "tenant-id-1", "tenant-id-2" };
|
|
256
|
+
var tenantId = issuer.Split('/')[3];
|
|
257
|
+
|
|
258
|
+
if (!allowedTenants.Contains(tenantId))
|
|
259
|
+
throw new SecurityTokenInvalidIssuerException("Tenant not allowed");
|
|
260
|
+
|
|
261
|
+
return issuer;
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Azure AD B2C
|
|
267
|
+
|
|
268
|
+
```csharp
|
|
269
|
+
// appsettings.json
|
|
270
|
+
{
|
|
271
|
+
"AzureAdB2C": {
|
|
272
|
+
"Instance": "https://yourtenant.b2clogin.com",
|
|
273
|
+
"Domain": "yourtenant.onmicrosoft.com",
|
|
274
|
+
"TenantId": "your-tenant-id",
|
|
275
|
+
"ClientId": "your-client-id",
|
|
276
|
+
"SignUpSignInPolicyId": "B2C_1_signupsignin",
|
|
277
|
+
"ResetPasswordPolicyId": "B2C_1_passwordreset",
|
|
278
|
+
"EditProfilePolicyId": "B2C_1_editprofile"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Program.cs
|
|
283
|
+
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
284
|
+
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C"));
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Documentação de Referência
|
|
288
|
+
|
|
289
|
+
- [Microsoft Identity Platform](https://learn.microsoft.com/en-us/entra/identity-platform/)
|
|
290
|
+
- [Microsoft.Identity.Web](https://learn.microsoft.com/en-us/entra/msal/dotnet/)
|
|
291
|
+
- [Blazor + Azure AD](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/)
|
|
292
|
+
- [Microsoft Graph](https://learn.microsoft.com/en-us/graph/overview)
|
|
293
|
+
- [Azure AD B2C](https://learn.microsoft.com/en-us/azure/active-directory-b2c/)
|
|
294
|
+
|
|
295
|
+
## Checklist de Integração
|
|
296
|
+
|
|
297
|
+
- [ ] App registrado no Azure Portal
|
|
298
|
+
- [ ] Client ID e Tenant ID configurados
|
|
299
|
+
- [ ] Client Secret no Key Vault
|
|
300
|
+
- [ ] Redirect URIs configurados
|
|
301
|
+
- [ ] API permissions definidas
|
|
302
|
+
- [ ] Token caching configurado
|
|
303
|
+
- [ ] Authorization policies criadas
|
|
304
|
+
- [ ] Logout flow implementado
|
|
305
|
+
- [ ] Error handling para tokens expirados
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
*MORPH-SPEC by Polymorphism Tech*
|