Skip to content

Commit

Permalink
feat(API): add grpc API and service account authentication (#33)
Browse files Browse the repository at this point in the history
Closes #8.

This adds the possibility to authenticate via Service Accounts (with the JWT profile) and then
use tokens in the API to call functions on zitadel.ch
  • Loading branch information
buehler authored Mar 25, 2021
1 parent 29ef145 commit c344500
Show file tree
Hide file tree
Showing 33 changed files with 8,630 additions and 15 deletions.
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-grpc": {
"version": "2.36.0",
"commands": [
"dotnet-grpc"
]
}
}
}
4 changes: 4 additions & 0 deletions .github/workflows/dotnet-pack-master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Test
run: ./build.sh --target test --no-logo
pack:
Expand All @@ -26,6 +28,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Pack
run: ./build.sh --no-logo --version 0.0.0-dev.${GITHUB_SHA::8} --release-notes 'This is a package built from master branch.' --target Pack
- uses: actions/upload-artifact@v2
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/dotnet-release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: .NET Release

on: [workflow_dispatch]
on:
push:
branches:
- master
- next

jobs:
test:
Expand All @@ -11,6 +15,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Test
run: ./build.sh --target test --no-logo
semantic-release:
Expand All @@ -24,6 +30,8 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v2
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Setup dotnet tools
run: dotnet tool restore
- name: Test
run: ./build.sh --target test --no-logo
6 changes: 3 additions & 3 deletions .github/workflows/security-analysis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Code Security Testing

on:
push:
branches:
- master
pull_request:
branches:
- master
Expand Down Expand Up @@ -39,6 +36,9 @@ jobs:
with:
dotnet-version: 5.0.100

- name: Setup dotnet tools
run: dotnet tool restore

- name: Build
run: ./build.sh --target compile --no-logo

Expand Down
16 changes: 13 additions & 3 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"verifyConditions": ["@semantic-release/github"],
"addChannel": ["@semantic-release/github"],
"branches": [
"master",
{
"name": "next",
"prerelease": "prerelease"
}
],
"plugins": ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/github"],
"prepare": [
[
"@semantic-release/exec",
Expand All @@ -13,7 +19,11 @@
[
"@semantic-release/github",
{
"assets": [{ "path": "artifacts/*.nupkg" }]
"assets": [
{
"path": "artifacts/*.nupkg"
}
]
}
],
[
Expand Down
14 changes: 14 additions & 0 deletions Zitadel.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Spa.Dev", "tests\Zi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Test", "tests\Zitadel.Test\Zitadel.Test.csproj", "{44543DC1-97C5-4525-8EE4-16E5389FDF4E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Api", "src\Zitadel.Api\Zitadel.Api.csproj", "{0F83D36E-945F-4B70-A6E9-867C21C546EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zitadel.Api.Access.Dev", "tests\Zitadel.Api.Access.Dev\Zitadel.Api.Access.Dev.csproj", "{B51464E4-58A6-4C41-A0BF-35245F0DD862}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -43,6 +47,8 @@ Global
{9AD0581A-82BA-4E96-A1EE-722D579C26D8} = {2462186B-61A8-476C-9674-176570BBEC35}
{23DE9A2C-E9A0-458A-8878-9224C49FFB3B} = {2462186B-61A8-476C-9674-176570BBEC35}
{44543DC1-97C5-4525-8EE4-16E5389FDF4E} = {2462186B-61A8-476C-9674-176570BBEC35}
{0F83D36E-945F-4B70-A6E9-867C21C546EB} = {47CEB49C-56A9-4BDF-BC66-54E407391D49}
{B51464E4-58A6-4C41-A0BF-35245F0DD862} = {2462186B-61A8-476C-9674-176570BBEC35}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BF13C638-6A5C-4BF6-89FF-1BAA3986F86A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand All @@ -63,5 +69,13 @@ Global
{44543DC1-97C5-4525-8EE4-16E5389FDF4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44543DC1-97C5-4525-8EE4-16E5389FDF4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44543DC1-97C5-4525-8EE4-16E5389FDF4E}.Release|Any CPU.Build.0 = Release|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F83D36E-945F-4B70-A6E9-867C21C546EB}.Release|Any CPU.Build.0 = Release|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B51464E4-58A6-4C41-A0BF-35245F0DD862}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
49 changes: 49 additions & 0 deletions src/Zitadel.Api/ClientOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using Zitadel.Authentication;
using Zitadel.Authentication.Credentials;

namespace Zitadel.Api
{
/// <summary>
/// Options for an API client.
/// </summary>
public record ClientOptions
{
/// <summary>
/// The API endpoint for the client. This will be the base url for the api calls.
/// </summary>
public string Endpoint { get; init; } = ZitadelDefaults.ZitadelApiEndpoint;

/// <summary>
/// The organizational context in the API. This essentially defines the "x-zitadel-orgid" header value
/// which provides the api with the orgId that the API call will be executed in.
/// This may be overwritten for specific calls.
/// </summary>
public string Organization { get; init; } = string.Empty;

/// <summary>
/// Authentication token for the client. This field may not be used in conjunction with
/// <see cref="ServiceAccountAuthentication"/>. Use this field to explicitly set the
/// Bearer token that will be transmitted to the API. If no authentication method is set,
/// each call must attach the authorization header.
/// </summary>
public string? Token { get; init; }

/// <summary>
/// Service Account authentication method. If this field is set, the API calls are
/// automatically authenticated with a <see cref="ServiceAccount"/> and the corresponding
/// <see cref="ServiceAccount.AuthOptions"/>. This will renew the access token if it is
/// expired.
/// </summary>
public (ServiceAccount Account, ServiceAccount.AuthOptions AuthOptions)? ServiceAccountAuthentication
{
get;
init;
}

/// <summary>
/// List of additional arbitrary headers that are attached to each call.
/// </summary>
public IDictionary<string, string>? AdditionalHeaders { get; init; }
}
}
75 changes: 75 additions & 0 deletions src/Zitadel.Api/Clients.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Net.Http;
using Caos.Zitadel.Admin.Api.V1;
using Caos.Zitadel.Auth.Api.V1;
using Caos.Zitadel.Management.Api.V1;
using Grpc.Core;
using Grpc.Net.Client;
using Zitadel.Authentication;

namespace Zitadel.Api
{
/// <summary>
/// Helper class to instantiate api service clients for the zitadel API with correct settings.
/// </summary>
public static class Clients
{
/// <summary>
/// Create a service client for the auth service.
/// </summary>
/// <param name="options">Options for the client like authorization method.</param>
/// <returns>The <see cref="Caos.Zitadel.Auth.Api.V1.AuthService.AuthServiceClient"/>.</returns>
public static AuthService.AuthServiceClient AuthService(ClientOptions options) =>
GetClient<AuthService.AuthServiceClient>(options);

/// <summary>
/// Create a service client for the admin service.
/// </summary>
/// <param name="options">Options for the client like authorization method.</param>
/// <returns>The <see cref="Caos.Zitadel.Admin.Api.V1.AdminService.AdminServiceClient"/>.</returns>
public static AdminService.AdminServiceClient AdminService(ClientOptions options) =>
GetClient<AdminService.AdminServiceClient>(options);

/// <summary>
/// Create a service client for the management service.
/// </summary>
/// <param name="options">Options for the client like authorization method.</param>
/// <returns>The <see cref="Caos.Zitadel.Management.Api.V1.ManagementService.ManagementServiceClient"/>.</returns>
public static ManagementService.ManagementServiceClient ManagementService(ClientOptions options) =>
GetClient<ManagementService.ManagementServiceClient>(options);

private static TClient GetClient<TClient>(ClientOptions options)
where TClient : ClientBase<TClient>
{
var httpClient = options.Token == null && options.ServiceAccountAuthentication != null
? new HttpClient(
new ServiceAccountHttpHandler(
options.ServiceAccountAuthentication.Value.Account,
options.ServiceAccountAuthentication.Value.AuthOptions))
: new HttpClient();

httpClient.DefaultRequestHeaders.Add(ZitadelDefaults.ZitadelOrgIdHeader, options.Organization);

if (options.Token != null)
{
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", options.Token);
}

if (options.AdditionalHeaders != null)
{
foreach (var (name, value) in options.AdditionalHeaders)
{
httpClient.DefaultRequestHeaders.Add(name, value);
}
}

var channel = GrpcChannel.ForAddress(
options.Endpoint,
new GrpcChannelOptions { HttpClient = httpClient });
var serviceType = typeof(TClient);

return Activator.CreateInstance(serviceType, channel) as TClient ??
throw new($"Could not instantiate type {serviceType}");
}
}
}
49 changes: 49 additions & 0 deletions src/Zitadel.Api/ServiceAccountHttpHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Zitadel.Authentication.Credentials;

namespace Zitadel.Api
{
internal class ServiceAccountHttpHandler : DelegatingHandler
{
private static readonly TimeSpan ServiceTokenLifetime = TimeSpan.FromHours(12);

private readonly ServiceAccount _account;
private readonly ServiceAccount.AuthOptions _options;

private DateTime _tokenExpiryDate;
private string? _token;

public ServiceAccountHttpHandler(ServiceAccount account, ServiceAccount.AuthOptions options)
: base(new HttpClientHandler())
{
_account = account;
_options = options;
}

protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
=> SendAsync(request, cancellationToken).Result;

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.Headers.Authorization != null)
{
return await base.SendAsync(request, cancellationToken);
}

// When the token is not fetched or it is expired, re-fetch a service account token.
if (_token == null || _tokenExpiryDate < DateTime.UtcNow)
{
_token = await _account.AuthenticateAsync(_options);
_tokenExpiryDate = DateTime.UtcNow + ServiceTokenLifetime;
}

request.Headers.Authorization = new("Bearer", _token);
return await base.SendAsync(request, cancellationToken);
}
}
}
Loading

0 comments on commit c344500

Please sign in to comment.