Skip to content

Commit d84cb2c

Browse files
authored
[OIDC] Add method to create a short-lived API key (minimal) (#10267)
This is a stub implementation until we have finalized the new API key design.
1 parent db81abe commit d84cb2c

3 files changed

Lines changed: 108 additions & 0 deletions

File tree

src/NuGetGallery.Services/Authentication/CredentialBuilder.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using NuGet.Services.Entities;
88
using NuGetGallery.Authentication;
9+
using NuGetGallery.Services.Authentication;
910

1011
namespace NuGetGallery.Infrastructure.Authentication
1112
{
@@ -28,6 +29,28 @@ public Credential CreatePasswordCredential(string plaintextPassword)
2829
V3Hasher.GenerateHash(plaintextPassword));
2930
}
3031

32+
public Credential CreateShortLivedApiKey(TimeSpan expiration, FederatedCredentialPolicy policy, out string plaintextApiKey)
33+
{
34+
if (policy.PackageOwner is null)
35+
{
36+
throw new ArgumentException($"The {nameof(policy.PackageOwner)} property on the policy must not be null.");
37+
}
38+
39+
if (expiration <= TimeSpan.Zero || expiration > TimeSpan.FromHours(1))
40+
{
41+
throw new ArgumentOutOfRangeException(nameof(expiration));
42+
}
43+
44+
// TODO: introduce a new API key type for short-lived API keys
45+
// Tracking: https://github.com/NuGet/NuGetGallery/issues/10212
46+
var credential = CreateApiKey(expiration, out plaintextApiKey);
47+
48+
credential.Description = "Short-lived API key generated via a federated credential";
49+
credential.Scopes = [new Scope(policy.PackageOwner, NuGetPackagePattern.AllInclusivePattern, NuGetScopes.All)];
50+
51+
return credential;
52+
}
53+
3154
public Credential CreateApiKey(TimeSpan? expiration, out string plaintextApiKey)
3255
{
3356
var apiKey = ApiKeyV4.Create();

src/NuGetGallery.Services/Authentication/ICredentialBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ public interface ICredentialBuilder
2020
IList<Scope> BuildScopes(User scopeOwner, string[] scopes, string[] subjects);
2121

2222
bool VerifyScopes(User currentUser, IEnumerable<Scope> scopes);
23+
24+
Credential CreateShortLivedApiKey(TimeSpan expiration, FederatedCredentialPolicy policy, out string plaintextApiKey);
2325
}
2426
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using NuGet.Services.Entities;
6+
using NuGetGallery.Authentication;
7+
using Xunit;
8+
9+
namespace NuGetGallery.Infrastructure.Authentication
10+
{
11+
public class CredentialBuilderFacts
12+
{
13+
public class TheCreateShortLivedApiKeyMethod : CredentialBuilderFacts
14+
{
15+
[Fact]
16+
public void CreatesShortLivedApiKey()
17+
{
18+
// Act
19+
var credential = Target.CreateShortLivedApiKey(Expiration, Policy, out var plaintextApiKey);
20+
21+
// Assert
22+
Assert.Null(credential.User);
23+
Assert.Equal(default, credential.UserKey);
24+
Assert.StartsWith("oy2", plaintextApiKey, StringComparison.Ordinal);
25+
Assert.Equal(CredentialTypes.ApiKey.V4, credential.Type);
26+
Assert.Equal("Short-lived API key generated via a federated credential", credential.Description);
27+
Assert.Equal(Expiration.Ticks, credential.ExpirationTicks);
28+
Assert.Null(credential.User);
29+
30+
var scope = Assert.Single(credential.Scopes);
31+
Assert.Equal(NuGetScopes.All, scope.AllowedAction);
32+
Assert.Equal(NuGetPackagePattern.AllInclusivePattern, scope.Subject);
33+
Assert.Same(Policy.PackageOwner, scope.Owner);
34+
}
35+
36+
[Fact]
37+
public void RejectsMissingPackageOwner()
38+
{
39+
// Arrange
40+
Policy.PackageOwner = null;
41+
42+
// Act
43+
Assert.Throws<ArgumentException>(() => Target.CreateShortLivedApiKey(Expiration, Policy, out var plaintextApiKey));
44+
}
45+
46+
[Theory]
47+
[InlineData(-1)]
48+
[InlineData(0)]
49+
[InlineData(61)]
50+
public void RejectsOutOfRangeExpiration(int expirationMinutes)
51+
{
52+
// Arrange
53+
Expiration = TimeSpan.FromMinutes(expirationMinutes);
54+
55+
// Act
56+
Assert.Throws<ArgumentOutOfRangeException>(() => Target.CreateShortLivedApiKey(Expiration, Policy, out var plaintextApiKey));
57+
}
58+
59+
public FederatedCredentialPolicy Policy { get; }
60+
61+
public TheCreateShortLivedApiKeyMethod()
62+
{
63+
Policy = new FederatedCredentialPolicy
64+
{
65+
Key = 23,
66+
PackageOwner = new User { Key = 42 },
67+
CreatedBy = new User { Key = 43 },
68+
};
69+
}
70+
}
71+
72+
public TimeSpan Expiration { get; set; }
73+
74+
public CredentialBuilder Target { get; }
75+
76+
public CredentialBuilderFacts()
77+
{
78+
Expiration = TimeSpan.FromMinutes(13);
79+
80+
Target = new CredentialBuilder();
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)