├── get_dev_token.ps1
├── get_prod_token.ps1
├── get_test_token.ps1
├── start_dev_token_server.ps1
├── start_prod_token_server.ps1
├── start_test_token_server.ps1
├── scripts
├── scopes.json
├── config.ps1
├── echo.php
├── serviceowners.json
├── org.ps1
├── env.ps1
├── scopes-admin.config.ps1
├── token.ps1
├── README.md
├── scopeaccess.ps1
├── so-patch.ps1
├── so-admin.ps1
└── scope.ps1
├── src
├── MaskinportenTokenGenerator
│ ├── MaskinportenTokenGenerator.csproj
│ ├── Server.cs
│ ├── TokenHandler.cs
│ └── Program.cs
└── MaskinportenTokenGenerator.sln
├── config-idporten-example.ps1
├── LICENSE
├── convert_batch_to_ps1.sh
├── config.ps1
├── README.md
├── maskinporten_token_generator.ps1
└── .gitignore
/get_dev_token.ps1:
--------------------------------------------------------------------------------
1 | if ($args.Length -gt 0) {
2 | & "$PSScriptRoot/maskinporten_token_generator.ps1" single dev $args[0]
3 | } else {
4 | & "$PSScriptRoot/maskinporten_token_generator.ps1" single dev
5 | }
6 |
--------------------------------------------------------------------------------
/get_prod_token.ps1:
--------------------------------------------------------------------------------
1 | if ($args.Length -gt 0) {
2 | & "$PSScriptRoot/maskinporten_token_generator.ps1" single prod $args[0]
3 | } else {
4 | & "$PSScriptRoot/maskinporten_token_generator.ps1" single prod
5 | }
6 |
--------------------------------------------------------------------------------
/get_test_token.ps1:
--------------------------------------------------------------------------------
1 | if ($args.Length -gt 0) {
2 | & "$PSScriptRoot/maskinporten_token_generator.ps1" single test $args[0]
3 | } else {
4 | & "$PSScriptRoot/maskinporten_token_generator.ps1" single test
5 | }
6 |
--------------------------------------------------------------------------------
/start_dev_token_server.ps1:
--------------------------------------------------------------------------------
1 | if ($args.Length -gt 0) {
2 | & "$PSScriptRoot/maskinporten_token_generator.ps1" servermode dev $args[0]
3 | } else {
4 | & "$PSScriptRoot/maskinporten_token_generator.ps1" servermode dev
5 | }
6 |
--------------------------------------------------------------------------------
/start_prod_token_server.ps1:
--------------------------------------------------------------------------------
1 | if ($args.Length -gt 0) {
2 | & "$PSScriptRoot/maskinporten_token_generator.ps1" servermode prod $args[0]
3 | } else {
4 | & "$PSScriptRoot/maskinporten_token_generator.ps1" servermode prod
5 | }
6 |
--------------------------------------------------------------------------------
/start_test_token_server.ps1:
--------------------------------------------------------------------------------
1 | if ($args.Length -gt 0) {
2 | & "$PSScriptRoot/maskinporten_token_generator.ps1" servermode test $args[0]
3 | } else {
4 | & "$PSScriptRoot/maskinporten_token_generator.ps1" servermode test
5 | }
6 |
--------------------------------------------------------------------------------
/scripts/scopes.json:
--------------------------------------------------------------------------------
1 | {
2 | "scopes": [
3 | "altinn:serviceowner",
4 | "altinn:serviceowner/instances.read",
5 | "altinn:serviceowner/instances.write",
6 | "altinn:serviceowner/notifications.create",
7 | "altinn:events.publish"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/scripts/config.ps1:
--------------------------------------------------------------------------------
1 | #### Configuration file for scopes admin scripts
2 |
3 | $global:CONFIG = @{
4 |
5 | # Absolute path to the tokengenerator .cmd-file
6 | TokenGenerator = "$PSScriptRoot/../maskinporten_token_generator.ps1"
7 |
8 | # If set to true, will always fetch a new token from, regardless of TTL.
9 | AlwaysRefreshToken = $false
10 |
11 |
12 | }
--------------------------------------------------------------------------------
/scripts/echo.php:
--------------------------------------------------------------------------------
1 | $v) {
6 | p("$k: $v\n");
7 | }
8 | p("\n");
9 | p(file_get_contents("php://input"));
10 |
11 | function p($str) {
12 | $stdout = fopen('php://stdout', 'w');
13 | fwrite($stdout, $str);
14 | }
--------------------------------------------------------------------------------
/scripts/serviceowners.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgs": {
3 | "digdir": {
4 | "name": {
5 | "en": "Norwegian Digitalisation Agency",
6 | "nb": "Digitaliseringsdirektoratet",
7 | "nn": "Digitaliseringsdirektoratet"
8 | },
9 | "orgnr": "991825827",
10 | "environments": [
11 | "test1",
12 | "ver1",
13 | "ver2",
14 | "prod"
15 | ]
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/MaskinportenTokenGenerator/MaskinportenTokenGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | Exe
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/scripts/org.ps1:
--------------------------------------------------------------------------------
1 | param (
2 | [Parameter(Mandatory=$true)][string]$orgno
3 | )
4 |
5 | try {
6 | $unit = Invoke-RestMethod -Uri ("https://data.brreg.no/enhetsregisteret/api/enheter/" + $orgno)
7 | }
8 | catch {
9 | if ($_.Exception.Response.StatusCode -eq 404) {
10 | try {
11 | $unit = Invoke-RestMethod -Uri ("https://data.brreg.no/enhetsregisteret/api/underenheter/" + $orgno)
12 | }
13 | catch {
14 | Write-Warning ("Server gave status code: " + $_.Exception.Response.StatusCode + " " + $_.Exception.Response.ReasonPhrase)
15 | Exit 1
16 | }
17 | }
18 | else {
19 | Write-Warning ("Server gave status code: " + $_.Exception.Response.StatusCode + " " + $_.Exception.Response.ReasonPhrase)
20 | Exit 1
21 | }
22 | }
23 |
24 | $unit
--------------------------------------------------------------------------------
/config-idporten-example.ps1:
--------------------------------------------------------------------------------
1 | # --------- TEST SETTINGS -----------
2 |
3 | # Whether or not personal login is request (ID-porten mode)
4 | $test_person_mode = "true"
5 |
6 | # The client_id that you have provisioned with the scopes you want
7 | $test_client_id = "yourclientidhere"
8 |
9 | # The thumbprint for your own enterprise certificate in local machine storage (Cert:\LocalMachine\My). This must match the orgno for the owner of the client.
10 | $test_certificate_thumbprint = "yourcertificatethumprinthere "
11 |
12 | # If you want to use CurrentUser certificate store location instead (Cert:\CurrentUser\My)
13 | $test_use_current_user_store_location = "true"
14 |
15 | # The scopes you want in your access token
16 | $test_scopes = ""yourscopehere""
17 |
18 | # The aud claim for the bearer grant assertion. Used as issuer claim in returned token
19 | $test_audience = "https://test.idporten.no"
20 |
21 | # Endpoint for token
22 | $test_token_endpoint = "https://test.idporten.no/token"
23 |
24 | # Endpoint for authorization (person mode)
25 | $test_authorize_endpoint = "https://login.test.idporten.no/authorize"
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2020 Norwegian Digitalisation Agency
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scripts/env.ps1:
--------------------------------------------------------------------------------
1 | $validenv = "prod", "test"
2 |
3 | if ($validenv -notcontains $env) {
4 | Write-Error ("Invalid env supplied. Valid environments: " + $validenv)
5 | Exit 1
6 | }
7 |
8 | if ($env -eq "prod") {
9 | $envurl = "https://api.samarbeid.digdir.no"
10 | }
11 | else {
12 | $envurl = "https://api.test.samarbeid.digdir.no"
13 | }
14 |
15 | #$envurl = "http://localhost:8000"
16 |
17 | function Invoke-API {
18 | param($Verb, $Path, $Body)
19 | $access_token = Get-Token
20 | $url = $envUrl + $Path
21 |
22 | try {
23 | $headers = @{
24 | Accept = "application/json"
25 | Authorization = "Bearer $access_token"
26 | }
27 | $result = Invoke-RestMethod -Uri $url -Method $Verb -Headers $headers -Body $Body -ContentType "application/json; charset=utf-8"
28 | }
29 | catch {
30 | Write-Warning "Request $verb $url failed"
31 | #Write-Warning ("Server gave status code: " + $_.Exception.Response.StatusCode + " " + $_.Exception.Response.ReasonPhrase)
32 | $_ | Format-List -Property * | Out-String | Write-Warning
33 | Exit 1
34 | }
35 |
36 | return $result
37 | }
--------------------------------------------------------------------------------
/scripts/scopes-admin.config.ps1:
--------------------------------------------------------------------------------
1 | # --------- PROD SETTINGS -----------
2 | # The path to a PKCS#12 file containing a certificate used to sign the request
3 | $production_keystore_path = "somepathto/keystore-prod.p12"
4 |
5 | # Password to the key store
6 | $production_keystore_password = "somepassword"
7 |
8 | # If authenticating with a pre-registered key (private_key_jwt), a kid must be supplied
9 | $production_kid = "somekid"
10 |
11 | # The client_id that you have provisioned with the scopes you want
12 | $production_client_id = "someclientid"
13 |
14 | # The scopes you want in your access token
15 | $production_scopes = "idporten:scopes.write"
16 |
17 | # --------- TETS SETTINGS -----------
18 | # The path to a PKCS#12 file containing a certificate used to sign the request
19 | $test_keystore_path = "somepathto/keystore-test.p12"
20 |
21 | # Password to the key store
22 | $test_keystore_password = "somepassword"
23 |
24 | # If authenticating with a pre-registered key (private_key_jwt), a kid must be supplied
25 | $test_kid = "somekid"
26 |
27 | # The client_id that you have provisioned with the scopes you want
28 | $test_client_id = "someclientid"
29 |
30 | # The scopes you want in your access token
31 | $test_scopes = "idporten:scopes.write"
32 |
--------------------------------------------------------------------------------
/src/MaskinportenTokenGenerator.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28917.181
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaskinportenTokenGenerator", "MaskinportenTokenGenerator\MaskinportenTokenGenerator.csproj", "{3B45A680-0345-41C0-950E-BA6A7618CE4E}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {3B45A680-0345-41C0-950E-BA6A7618CE4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {3B45A680-0345-41C0-950E-BA6A7618CE4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {3B45A680-0345-41C0-950E-BA6A7618CE4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {3B45A680-0345-41C0-950E-BA6A7618CE4E}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {9941479A-DEB7-4FB1-81EF-E6162BFD39F8}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/convert_batch_to_ps1.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Check if a file name is provided as an argument
4 | if [ $# -ne 1 ]; then
5 | echo "Usage: $0 "
6 | exit 1
7 | fi
8 |
9 | # Input batch file
10 | batch_file="$1"
11 |
12 | # Output PowerShell file
13 | ps_file="${batch_file%.*}.ps1"
14 | echo -n > "$ps_file"
15 |
16 | # Process each line of the batch file
17 | while IFS='' read -r line || [[ -n "$line" ]]; do
18 | # Remove carriage return from the line
19 | line=$(echo "$line" | tr -d '\r')
20 | if [[ $line =~ ^REM || $line =~ ^:: ]]; then
21 | # Convert to PowerShell comment, preserving leading whitespace for alignment
22 | echo "#${line#??}" >> "$ps_file"
23 | elif [[ $line =~ ^set ]]; then
24 | # Convert set directives to PowerShell variable assignments
25 | var_name=$(echo "$line" | sed -n 's/^set \([^=]*\)=.*/\1/p')
26 | var_value=$(echo "$line" | sed -n 's/^set [^=]*=\(.*\)/\1/p')
27 | # Ensure proper PowerShell syntax for variable assignment
28 | echo "\$$var_name = \"$var_value\"" >> "$ps_file"
29 | elif [ -z "$line" ]; then
30 | # Preserve empty lines without affecting variable values
31 | echo "" >> "$ps_file"
32 | fi
33 | done < "$batch_file"
34 |
35 | echo "Conversion complete. Output file: $ps_file"
36 |
--------------------------------------------------------------------------------
/scripts/token.ps1:
--------------------------------------------------------------------------------
1 |
2 | function IsTokenCacheExpired {
3 | param($TokenCache)
4 | $token = Get-Content -Path $token_cache
5 | $parts = $token.Split(".")
6 | $parts[1] = $parts[1] + ('=' * ($parts[1].Length % 4))
7 | try {
8 | $payload = ConvertFrom-JSON([Text.Encoding]::Utf8.GetString([Convert]::FromBase64String($parts[1])))
9 | }
10 | catch {
11 | return $True
12 | }
13 | $date = Get-Date "1/1/1970"
14 | $validto = $date.AddSeconds($payload.exp).ToLocalTime()
15 | if ($validto -lt (Get-Date)) {
16 | Write-Verbose "Token stale, expired at $validto"
17 | return $true
18 | }
19 | Write-Verbose "Token fresh, expires $validto"
20 | return $false
21 | }
22 |
23 | function Get-Token {
24 |
25 | if (!(Test-Path $CONFIG["TokenGenerator"])) {
26 | Write-Warning ($CONFIG["TokenGenerator"] + " not found, please check config.ps1")
27 | Exit 1
28 | }
29 |
30 | $tgconfig = $PSScriptRoot + "/scopes-admin.config.local.ps1";
31 | if (!(Test-Path $tgconfig)) {
32 | Write-Warning "$tgconfig not found. Copy it from scopes-admin.config.ps1"
33 | Exit 1
34 | }
35 |
36 | $token_cache = "./token.$env.cache"
37 |
38 | if ($CONFIG["AlwaysRefresh"] -eq $True -or !(Test-Path($token_cache)) -or (IsTokenCacheExpired($token_cache))) {
39 | $cmd = $CONFIG["TokenGenerator"] + " onlytoken " + $env + " " + $tgconfig
40 | Write-Verbose "Running: $cmd"
41 | & pwsh -Command $cmd *> $token_cache
42 | }
43 |
44 | $token = Get-Content -Path $token_cache
45 |
46 | if ($null -eq $token) {
47 | Write-Error "Did not get token"
48 | Remove-Item $token_cache -ErrorAction Ignore
49 | Exit 1
50 | }
51 |
52 | return $token
53 | }
54 |
--------------------------------------------------------------------------------
/scripts/README.md:
--------------------------------------------------------------------------------
1 | # Scope administration scripts
2 |
3 | This directory contains scripts that can be useful for performing/automating various administrative tasks related to ID/Maskinporten.
4 |
5 | > Warning! These are tools with sharp edges, capable of messing up your Maskinporten scope and access setup.
6 | > Use with extreme care!
7 |
8 | ## Setup
9 |
10 | Copy `scopes-admin-config.ps1` to `scopes-admin-config.local.ps1` and fill in credentials for a Maskinporten- client with `idporten:scopes.write` scope.
11 |
12 | ## Administrating scopes
13 |
14 | See `scope.ps1` for usage examples. Typical flow is:
15 |
16 | 1. Export all scopes definitions from Maskinporten to CSV, eg.:
17 | `./scope.ps1 export-to-csv -prefix altinn -Env test`
18 |
19 | 2. Edit the CSV to your liking:
20 |
21 | A very useful extension here is https://marketplace.visualstudio.com/items?itemName=janisdd.vscode-edit-csv, which lets you edit the CSV list of scopes visually.
22 |
23 | 3. Convert CSV to JSON, eg.
24 | `./scope.ps1 import-from-csv -file somefile.csv`
25 |
26 | 4. Apply the files you want, eg.
27 |
28 | `./scope.ps1 update -File .\exported\scopes\altinn\.json`
29 |
30 | You can use the following script to update all scopes. Use with extreme care!
31 |
32 | `Get-ChildItem -Recurse -Path .\imported\scopes\altinn\ -File -Filter *.json | Foreach-Object { Write-Host ".\scope.ps1 update -File" $_.FullName }`
33 |
34 | ## Administrating scope access
35 |
36 | See `scopeaccess.ps1` for usage examples.
37 |
38 | ## Administrating service owner access
39 |
40 | See `so-admin.ps1`.
41 |
42 | Used for easily granting all `altinn/serviceowner/*` scopes to all service owners as defined in serviceowners.json.
43 |
44 | ## Troubleshooting
45 |
46 | Most commands takes a `-Verbose` flag which prints additional information. In case of errors with authentication, see `token.${env}.cache` files. These can be deleted to force a new token retrieval.
--------------------------------------------------------------------------------
/config.ps1:
--------------------------------------------------------------------------------
1 | #
2 | # Do not alter this file directly, but copy it to config.local.cmd where you can override values defined in this file
3 | #
4 |
5 | # --------- PRODUCTION SETTINGS -----------
6 | # The thumbprint for your own enterprise certificate in local machine storage (Cert:\LocalMachine\My)
7 | $production_certificate_thumbprint = ""
8 |
9 | # If you want to use CurrentUser certificate store location instead (Cert:\CurrentUser\My)
10 | #$production_use_current_user_store_location = "true"
11 |
12 | # Or alternatively; the path to a PKCS#12 file containing a certificate used to sign the request
13 | #$production_keystore_path=
14 |
15 | # Password to the key store. Make sure you escape correctly.
16 | #$production_keystore_password=
17 |
18 | # Or alternatively; the path to a JWK file containing the public/private key used to sign the request
19 | #$production_jwk_path=
20 |
21 | # If authenticating with a pre-registered key, the kid used as identifier must be included in the assertion. If not supplied, falls back to thumbprint (same as x5t).
22 | #$production_kid=
23 |
24 | # The client_id that you have provisioned with the scopes you want
25 | $production_client_id = ""
26 |
27 | # The intended "aud" claim for the access_token
28 | $production_resource = ""
29 |
30 | # The scopes you want in your access token (comma delimited, no spaces)
31 | $production_scopes = ""
32 |
33 | # The aud claim for the bearer grant assertion. Used as issuer claim in returned token
34 | $production_audience = "https://maskinporten.no/"
35 | # For ID-porten: $production_audience=https://oidc.difi.no/idporten-oidc-provider/
36 |
37 | # Endpoint to send bearer grant assertion
38 | $production_token_endpoint = "https://maskinporten.no/token"
39 | # For ID-porten: $production_token_endpoint=https://oidc.difi.no/idporten-oidc-provider/token
40 |
41 | # Endpoint for authorization (only used for person mode)
42 | $production_authorize_endpoint = "https://login.idporten.no/authorize"
43 |
44 | # Enables login with a person in ID-porten and authCode flow. Implies server mode, and requires a ID-porten client configured with private_jwt authentication
45 | $production_person_mode = "false"
46 |
47 | # Enables supplier mode for use with Maskinporten and delegation schemes. Enter the organization number that will have to delegate access to this scope in Altinn
48 | $production_consumer_org = ""
49 |
50 | # --------- TEST (for ATxx/TT02) SETTINGS -----------
51 | $test_certificate_thumbprint = ""
52 | #$test_use_current_user_store_location=true
53 | #$test_keystore_path=
54 | #$test_keystore_password=
55 | #$test_jwk_path=
56 | #$test_kid=
57 | $test_client_id = ""
58 | $test_resource = ""
59 | $test_scopes = ""
60 | $test_audience = "https://test.maskinporten.no/"
61 | $test_token_endpoint = "https://test.maskinporten.no/token"
62 | $test_authorize_endpoint = "https://login.test.idporten.no/authorize"
63 | $test_person_mode = "false"
64 | $test_consumer_org = ""
65 |
66 | # --------- DEV SETTINGS. This is only for internal/experimental use, you probably want TEST -----------
67 | $dev_certificate_thumbprint = ""
68 | #$dev_use_current_user_store_location=true
69 | #$dev_keystore_path=
70 | #$dev_keystore_password=
71 | #$dev_jwk_path=
72 | #$dev_kid=
73 | $dev_client_id = ""
74 | $dev_resource = ""
75 | $dev_scopes = ""
76 | $dev_audience = "https://maskinporten.dev/"
77 | $dev_token_endpoint = "https://maskinporten.dev/token"
78 | $dev_authorize_endpoint = "https://login.idporten.dev/authorize"
79 | $dev_person_mode = "false"
80 | $dev_consumer_org = ""
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MaskinportenTokenGenerator
2 |
3 | This is a utilty for helping out with generating access_tokens from ID/Maskinporten, supporting integration with Postman for automating retrieval of access_tokens via a local web server.
4 |
5 | ## Requirements
6 | * .NET8 SDK for building
7 | * Powershell 7
8 | * Either
9 | * A enterprise certificate owned installed owned by the organization that has been given access to one or more scopes in Maskinporten installed in the certificate store (Windows only)
10 | * A JSON file containing a JWK. Used if the client has been configured with a pre-configured key. See https://mkjwk.org/ for examples on how to construct JWKs. NOTE! As of now only RS256 algorithm is supported.
11 | * A password-protected PKCS#12 file containing the public/private key pair. Can also be used if the client has been configured with a pre-configured key.
12 | * A client id for an integration in Maskinporten provisioned with one or more scopes
13 |
14 | ## Building
15 | Open and build in your favourite IDE, or run `dotnet build`
16 | ## Usage
17 | 1. Copy `config.ps1` to `config.local.ps1` and configure the production and/or TEST-settings
18 | 2. Run either of the following utility scripts:
19 | * `get_${env}_token` Gets a access_token and places it on the clipboard (for easy pasting in Postman etc)
20 | * `start_${env}_token_server` Starts a simple HTTP-server listening on all interfaces on port 17823 by default. Any GET-request to `http://localhost:17823` will attempt to fetch a access_token from Maskinporten and proxy the response.
21 |
22 | You can keep multiple configuration files for various settings, and can pass those as a single parameter to the scripts, like `start_test_token_server config.local.my-custom-config.ps1`
23 |
24 | This can also be done by dragging and dropping the custom config-file over the script you want to run.
25 |
26 | ## Postman integration
27 | By using the token server, you can add a "Pre-request script" in Postman, with somelike the following:
28 |
29 | /* Adding "?cache=true" returns the same token as long as it is valid (ie. does not request a new token from Maskinporten) */
30 | pm.sendRequest("http://localhost:17823/?cache=true", function (err, response) {
31 | var json = response.json();
32 | if (typeof json.access_token !== "undefined") {
33 | pm.environment.set("BearerToken", json.access_token);
34 | }
35 | else {
36 | console.error("Failed getting token", json);
37 | }
38 | });
39 |
40 | Here "BearerToken" is an environment variable, which can be put in the "Token"-field in the "Authorization"-tab when type is set to "Bearer Token".
41 |
42 | *If you are testing MaskinportenAPI, see https://github.com/Altinn/MaskinportenApiPostman for a pre-configured Postman collection*
43 |
44 | ## License
45 | MIT
46 |
47 | ## Changelog (since Sep. 2020)
48 | * 2024-02-07: Port to net8.0 (cross platform), and convert batch scripts to Powershell
49 | * 2023-10-19: Upgrade to net6.0-windows
50 | * 2023-06-08: Add support for PKCE in person-mode.
51 | * 2023-06-07: Set new "test" environment as default replacing "ver2".
52 | * 2022-07-21: Add support for supplying a JWK-file instead of PKCS#12 for self-generated keys
53 | * 2020-11-13: Bugfixes and refactorings
54 | * 2020-10-16: Added support for [supplier integrations](https://difi.github.io/felleslosninger/maskinporten_guide_apikonsument.html#bruke-delegering-som-leverand%C3%B8r) for delegated Maskinporten scopes
55 | * 2020-09-15: Added preliminary support for ID-porten personal login / authcode flow
56 | * 2020-09-15: Added scripts for managing scope access
57 |
--------------------------------------------------------------------------------
/scripts/scopeaccess.ps1:
--------------------------------------------------------------------------------
1 | # Script to manage access (whitelisting) to scopes defined in ID/Maskinporten
2 | # Author: bdl@digdir.no
3 | # -----------------------------------------------------------------------------------------------------------------
4 | # NOTE! Requires a "scopes-admin.config.local.ps1" file present in same directory as script.
5 | # -----------------------------------------------------------------------------------------------------------------
6 | #
7 | # Examples:
8 | # ./scopeaccess get someprefix:somescope -> Returns a list of organizations with access to someprefix:somescope
9 | # ./scopeaccess getorg 912345678 -> Returns a list of scopes granted an organization
10 | # ./scopeaccess get someprefix:somescope 912345678 -> Returns the scope access for a given scope and organization
11 | # ./scopeaccess remove someprefix:somescope 912345678 -> Revoke 912345678 access to someprefix:somescope
12 | # ./scopeaccess add someprefix:somescope 912345678 -> Grant 912345678 access to someprefix:somescope
13 | # ./scopeaccess listprefix someprefix:somescope -> List all scopes starting with someprefix:somescope
14 | #
15 | # Environment defaults to "TEST". Can be overridden by supplying a fourth positional or -env parameter containing "test" or "prod"
16 |
17 | param (
18 | [Parameter(Mandatory=$true)][string]$operator,
19 | [Parameter()][string]$scope,
20 | [Parameter()][string]$org,
21 | [Parameter()][string]$env = "test"
22 | )
23 |
24 | . ($PSScriptRoot + "/config.ps1")
25 | . ($PSScriptRoot + "/token.ps1")
26 | . ($PSScriptRoot + "/env.ps1")
27 |
28 | function Add-Scope-Access {
29 | param($Scope, $Org)
30 | $result = Invoke-API -Verb PUT -Path ("/scopes/access/" + $Org + "?scope=" + $scope)
31 | $result
32 | }
33 |
34 | function Remove-Scope-Access {
35 | param($Scope, $Org)
36 | $result = Invoke-API -Verb DELETE -Path ("/scopes/access/" + $Org + "?scope=" + $scope)
37 | $result
38 | }
39 |
40 | function Get-Scope-Access {
41 | param($Scope, $Org)
42 | if ($Scope -eq "") {
43 | $result = Invoke-API -Verb GET -Path "/scopes/access/?consumer_orgno=$Org"
44 | }
45 | else {
46 | $result = Invoke-API -Verb GET -Path "/scopes/access/?consumer_orgno=$Org&scope=$scope"
47 | }
48 | if ($null -eq $result) {
49 | Write-Output "No scope or org found"
50 | }
51 | else {
52 | $result
53 | }
54 | }
55 |
56 | function Get-Scope-Access-All {
57 | param($Scope)
58 | $result = Invoke-API -Verb GET -Path "/scopes/access/?scope=$scope"
59 | if ($null -eq $result) {
60 | Write-Output "No scope found"
61 | }
62 | else {
63 | $result
64 | }
65 | }
66 |
67 | function Get-All-Scopes-Starting-With {
68 | param($Prefix)
69 | $result = Invoke-API -Verb GET -Path "/scopes" | Where-Object { $_.name -match ("^" + $Prefix) }
70 | return $result
71 | }
72 |
73 | #####################################################
74 |
75 | if ($operator -eq "add") {
76 | if ($org -eq "") {
77 | Write-Error "Organization number must be supplied"
78 | Exit 1
79 | }
80 | Add-Scope-Access -Scope $scope -Org $org
81 | }
82 | elseif ($operator -eq "remove") {
83 | if ($org -eq "") {
84 | Write-Error "Organization number must be supplied"
85 | Exit 1
86 | }
87 | Remove-Scope-Access -Scope $scope -Org $org
88 | }
89 | elseif ($operator -eq "get") {
90 | if ($org -eq "") {
91 | Get-Scope-Access-All -Scope $scope
92 | }
93 | else {
94 | Get-Scope-Access -Scope $scope -Org $org
95 | }
96 | }
97 | elseif ($operator -eq "getorg") {
98 | if ($org -eq "") {
99 | Write-Error "Organization number must be supplied"
100 | Exit 1
101 | }
102 | Get-Scope-Access -Org $org -Scope ""
103 | }
104 | elseif ($operator -eq "listprefix") {
105 | Get-All-Scopes-Starting-With -Prefix $scope
106 | }
107 | else {
108 | Write-Error 'Operation must be one of "add", "remove", "get", "getorg" or "listprefix"'
109 | }
110 |
--------------------------------------------------------------------------------
/scripts/so-patch.ps1:
--------------------------------------------------------------------------------
1 | # Script to patch which scopes a service owner has access to based on the scopes defined in the scopes file.
2 | # Author: teh@digdir.no
3 | # -----------------------------------------------------------------------------------------------------------------
4 | # NOTE! Requires a "scopes-admin.config.local.cmd" file present in same directory as script.
5 | # -----------------------------------------------------------------------------------------------------------------
6 | #
7 | # Examples:
8 | # .\so-patch report -env test -org 991825827 --> Generates a report showing expected and actual access to scopes for one org
9 | # .\so-patch patch -env test -org 991825827 --> Gives org access to all scopes defined in scopes.local.json
10 | # .\so-patch patch_all -env test -scope someprefix:somescope --> Perfect if introducing a new scope you want all application owners to have access to
11 | #
12 |
13 | [cmdletbinding()]
14 | param (
15 | [Parameter(Mandatory=$true)][string]$Operator,
16 | [Parameter(Mandatory=$true)][string]$Env,
17 | [Parameter(Mandatory=$false)][string]$Org,
18 | [Parameter(Mandatory=$false)][string]$Scope
19 | )
20 |
21 | $ScopeAccess = ($PSScriptRoot + "\scopeaccess.ps1")
22 |
23 | function Get-Scopes {
24 | $scopesfile = "$PSScriptRoot\scopes.local.json"
25 | if (!(Test-Path $scopesfile)) {
26 | Write-Warning "$scopesfile not found. Copy it from scopes.json"
27 | Exit 1
28 | }
29 | $scopes = Get-Content -Raw -Path $scopesfile | ConvertFrom-Json
30 | $scopes.scopes
31 | }
32 |
33 | function Get-ServiceOwners {
34 | param($env)
35 |
36 | $sofile = "$PSScriptRoot\serviceowners.local.json"
37 | if (!(Test-Path $sofile)) {
38 | Write-Warning "$sofile not found. Copy it from serviceowners.json"
39 | Exit 1
40 | }
41 |
42 | $fileContent = Get-Content -Raw -Path $sofile | ConvertFrom-Json
43 |
44 | $so = @{}
45 | $fileContent.orgs.psobject.properties
46 | | ForEach-Object { $so[$_.Name] = $_.Value }
47 | $so.Values
48 | | Where-Object { $_.environments -and $_.environments.Contains($env) }
49 | | Select-Object -ExpandProperty name -Property orgnr
50 | | ForEach-Object {
51 | [PSCustomObject]@{
52 | OrgNo = $_.orgnr
53 | Name = $_.nb
54 | }}
55 | }
56 |
57 | function Get-ReportOrg {
58 | param($env, $org)
59 | $scopes = Get-Scopes
60 |
61 | $report = [ordered]@{}
62 | foreach ($scope in $scopes) {
63 | $report[$scope] = $false
64 | }
65 |
66 | $orgscopes = . $ScopeAccess -env $env -operator get -org $org
67 | foreach ($orgscope in $orgscopes) {
68 | Write-Verbose $orgscope
69 | if ($report.Keys -contains $orgscope.scope -and $orgscope.state -eq "APPROVED") {
70 | $report[$orgscope.scope] = $true
71 | }
72 | }
73 | $report
74 | }
75 |
76 | function Patch-Org {
77 | param($env, $org)
78 |
79 | $report = Get-ReportOrg -env $env -org $org
80 |
81 | $report | Format-Table -AutoSize
82 |
83 | foreach ($item in $report.GetEnumerator()) {
84 | if ($item.Value -eq $false) {
85 | Write-Warning ("Giving $org access to " + $item.Key)
86 | . $ScopeAccess -env $env -operator add -scope $item.Key -org $org
87 | }
88 | }
89 | }
90 |
91 | function Patch-All {
92 | param($env, $scope)
93 |
94 | $scopes = Get-Scopes
95 |
96 | if ($scopes -notcontains $scope) {
97 | Write-Error "Scope $scope not found in scopes.local.json"
98 | Exit 1
99 | }
100 |
101 | $orgs = Get-ServiceOwners -env $env
102 |
103 | foreach ($org in $orgs) {
104 | Write-Verbose ("Checking " + $org.Name + " ..." + $org.OrgNo)
105 |
106 | $report = Get-ReportOrg -env $env -org $org.OrgNo
107 | $report | Format-Table -AutoSize
108 |
109 | if ($report[$scope] -eq $false) {
110 | Write-Warning ("Giving " + $org.Name + " access to " + $scope)
111 | . $ScopeAccess -env $env -operator add -scope $scope -org $org.OrgNo
112 | }
113 | }
114 | }
115 |
116 |
117 | # ----------------------------------------------------------------------------------------------------------------- #
118 |
119 | if ($Operator -eq "report") {
120 | if ($Org -eq "") {
121 | Write-Error "Organization number must be supplied"
122 | Exit 1
123 | }
124 | Get-ReportOrg -env $Env -org $Org | Format-Table -AutoSize
125 | }
126 |
127 | if ($Operator -eq "patch") {
128 | if ($Org -eq "") {
129 | Write-Error "Organization number must be supplied"
130 | Exit 1
131 | }
132 | Patch-Org -env $Env -org $Org
133 | }
134 |
135 | if ($Operator -eq "patch_all") {
136 | if ($Scope -eq "") {
137 | Write-Error "A scope must be supplied. Also remember that the scope must be defined in scopes.local.json"
138 | Exit 1
139 | }
140 | Patch-All -env $Env -scope $Scope
141 | }
142 |
--------------------------------------------------------------------------------
/maskinporten_token_generator.ps1:
--------------------------------------------------------------------------------
1 | # Set script directory as the current working directory
2 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
3 | Set-Location $PSScriptRoot
4 |
5 | $MPEXE = Join-Path $PSScriptRoot "src\MaskinportenTokenGenerator\bin\Debug\net8.0\MaskinportenTokenGenerator.dll"
6 | if (-not (Test-Path $MPEXE)) {
7 | Write-Host "$MPEXE not found. Build it first."
8 | Pause
9 | exit 1
10 | }
11 |
12 | $server_mode_opt = $null
13 | $only_token_opt = $null
14 | if ($args[0] -eq "servermode") {
15 | $server_mode_opt = "--server_mode --server_port=17823"
16 | }
17 |
18 | if ($args[0] -eq "onlytoken") {
19 | $only_token_opt = "--only_token"
20 | }
21 |
22 | $local_config = $null
23 | if ([string]::IsNullOrEmpty($args[2])) {
24 | $local_config = "config.local.ps1"
25 | } else {
26 | if (-not (Test-Path $args[2])) {
27 | Write-Host "Unable to load custom config file: $($args[2])"
28 | Pause
29 | exit 1
30 | }
31 | if ([string]::IsNullOrEmpty($only_token_opt)) {
32 | Write-Host "Using custom config file: $($args[2])"
33 | }
34 | $local_config = $args[2]
35 | }
36 |
37 | . .\config.ps1
38 | if (Test-Path $local_config) {
39 | if (-not [System.IO.Path]::IsPathRooted($local_config)) {
40 | $local_config = "./$local_config"
41 | }
42 |
43 | . $local_config
44 | }
45 |
46 | $certificate_thumbprint = $null
47 | $keystore_path = $null
48 | $keystore_password = $null
49 | $jwk_path = $null
50 | $kid = $null
51 | $client_id = $null
52 | $resource = $null
53 | $scopes = $null
54 | $audience = $null
55 | $token_endpoint = $null
56 | $authorize_endpoint = $null
57 | $person_mode = $null
58 | $consumer_org = $null
59 | $use_current_user_store_location = $null
60 |
61 | if ($args[1] -eq "dev") {
62 | if ([string]::IsNullOrEmpty($dev_client_id)) {
63 | Write-Host "Missing configuration for DEV environment. Check the configuration, and make sure that any config.local.ps1 is up-to-date with fields defined in config.ps1"
64 | Pause
65 | exit 1
66 | }
67 |
68 | $certificate_thumbprint = $dev_certificate_thumbprint
69 | $keystore_path = $dev_keystore_path
70 | $keystore_password = $dev_keystore_password
71 | $jwk_path = $dev_jwk_path
72 | $kid = $dev_kid
73 | $client_id = $dev_client_id
74 | $resource = $dev_resource
75 | $scopes = $dev_scopes
76 | $audience = $dev_audience
77 | $token_endpoint = $dev_token_endpoint
78 | $authorize_endpoint = $dev_authorize_endpoint
79 | $person_mode = $dev_person_mode
80 | $consumer_org = $dev_consumer_org
81 | $use_current_user_store_location = $dev_use_current_user_store_location
82 | }
83 |
84 | if ($args[1] -eq "test") {
85 | if ([string]::IsNullOrEmpty($test_client_id)) {
86 | Write-Host "Missing configuration for TEST/ATxx/TT02 environment. Check the configuration, and make sure that any config.local.ps1 is up-to-date with fields defined in config.ps1"
87 | Pause
88 | exit 1
89 | }
90 |
91 | $certificate_thumbprint = $test_certificate_thumbprint
92 | $keystore_path = $test_keystore_path
93 | $keystore_password = $test_keystore_password
94 | $jwk_path = $test_jwk_path
95 | $kid = $test_kid
96 | $client_id = $test_client_id
97 | $resource = $test_resource
98 | $scopes = $test_scopes
99 | $audience = $test_audience
100 | $token_endpoint = $test_token_endpoint
101 | $authorize_endpoint = $test_authorize_endpoint
102 | $person_mode = $test_person_mode
103 | $consumer_org = $test_consumer_org
104 | $use_current_user_store_location = $test_use_current_user_store_location
105 | }
106 |
107 | if ($args[1] -eq "prod") {
108 | if ([string]::IsNullOrEmpty($production_client_id)) {
109 | Write-Host "Missing configuration for PROD environment. Check the configuration, and make sure that any config.local.ps1 is up-to-date with fields defined in config.ps1"
110 | Pause
111 | exit 1
112 | }
113 |
114 | $certificate_thumbprint = $production_certificate_thumbprint
115 | $keystore_path = $production_keystore_path
116 | $keystore_password = $production_keystore_password
117 | $jwk_path = $production_jwk_path
118 | $kid = $production_kid
119 | $client_id = $production_client_id
120 | $resource = $production_resource
121 | $scopes = $production_scopes
122 | $audience = $production_audience
123 | $token_endpoint = $production_token_endpoint
124 | $authorize_endpoint = $production_authorize_endpoint
125 | $person_mode = $production_person_mode
126 | $consumer_org = $production_consumer_org
127 | $use_current_user_store_location = $production_use_current_user_store_location
128 | }
129 |
130 | $resource_opt = if ($resource) { "--resource=$resource" } else { $null }
131 | $certificate_thumbprint_opt = if ($certificate_thumbprint) { "--certificate_thumbprint=$certificate_thumbprint" } else { $null }
132 | $keystore_opt = if ($keystore_path) { "--keystore_path=$keystore_path --keystore_password=$keystore_password" } else { $null }
133 | $jwk_path_opt = if ($jwk_path) { "--jwk_path=$jwk_path" } else { $null }
134 | $kid_opt = if ($kid) { "--kid=$kid" } else { $null }
135 | $person_mode_opt = if ($person_mode) { "--person_mode=$person_mode" } else { $null }
136 | $consumer_org_opt = if ($consumer_org) { "--consumer_org=$consumer_org" } else { $null }
137 | $use_current_user_store_location_opt = if ($use_current_user_store_location) { "--use_current_user_store_location=$use_current_user_store_location" } else { $null }
138 |
139 | $cmd = "dotnet $MPEXE --client_id=$client_id --audience=$audience --token_endpoint=$token_endpoint --authorize_endpoint=$authorize_endpoint --scopes=$scopes $only_token_opt $person_mode_opt $server_mode_opt $resource_opt $certificate_thumbprint_opt $keystore_opt $jwk_path_opt $kid_opt $consumer_org_opt $use_current_user_store_location_opt"
140 | if (-not $only_token_opt) {
141 | Write-Host "-------------------------------"
142 | Write-Host $cmd
143 | Write-Host "-------------------------------"
144 | }
145 | Invoke-Expression $cmd
146 |
--------------------------------------------------------------------------------
/scripts/so-admin.ps1:
--------------------------------------------------------------------------------
1 | # Script to updates service owner access to Altinn service owner scopes defined in ID/Maskinporten
2 | # Author: bdl@digdir.no
3 | # -----------------------------------------------------------------------------------------------------------------
4 | # NOTE! Requires a "scopes-admin.config.local.cmd" file present in same directory as script.
5 | # -----------------------------------------------------------------------------------------------------------------
6 | #
7 | # Examples:
8 | # ./so-admin -Env test -Report --> Generates a report showing all orgs having access to a scope
9 | # ./so-admin -Env test -ShowMissing --> List all serviceowners missing scope access
10 | # ./so-admin -Env test -ShowExtra --> List all scopes having orgs with access that are not serviceowners
11 | # ./so-admin -Env test -AddMissing --> Grant service owners access to missing scopes, if any
12 | # ./so-admin -Env test -RemoveExtra --> Revoke non-service owners access to scopes, if any
13 | # ./so-admin -Env test -RemoveSingle -->
14 | # ./so-admin -Env test -AddSingle -->
15 |
16 | [cmdletbinding()]
17 | param (
18 | [Parameter(Mandatory=$true)][string]$Env,
19 | [Parameter(Mandatory=$false)][string]$Org,
20 | [Parameter(ParameterSetName="Report")][Switch]$Report,
21 | [Parameter(ParameterSetName="ShowMissing")][Switch]$ShowMissing,
22 | [Parameter(ParameterSetName="ShowExtra")][Switch]$ShowExtra,
23 | [Parameter(ParameterSetName="AddMissing")][Switch]$AddMissing,
24 | [Parameter(ParameterSetName="RemoveExtra")][Switch]$RemoveExtra,
25 | [Parameter(ParameterSetName="RemoveSingle")][Switch]$RemoveSingle,
26 | [Parameter(ParameterSetName="AddSingle")][Switch]$AddSingle
27 | )
28 |
29 | $ScopeAccess = ($PSScriptRoot + "\scopeaccess.ps1")
30 |
31 | function Get-ServiceOwners {
32 | param($Env)
33 | $sofile = "$PSScriptRoot\serviceowners.local.json"
34 | if (!(Test-Path $sofile)) {
35 | Write-Warning "$sofile not found. Copy it from serviceowners.json"
36 | Exit 1
37 | }
38 | $tmp = Get-Content -Raw -Path $sofile | ConvertFrom-Json
39 | $so = @{}
40 | $tmp.orgs.psobject.properties | ForEach-Object { $so[$_.Name] = $_.Value }
41 | $so.Values | Where-Object { $_.environments -and $_.environments.Contains($Env) }
42 | }
43 |
44 | function Generate-Full-Report {
45 | $report = @{}
46 | Write-Verbose "Getting list of serviceowner scopes ..."
47 | $scopes = . $ScopeAccess -env $Env -operator listprefix -scope altinn:serviceowner | ForEach-Object { $_.name }
48 | Write-Verbose ($scopes.Count.ToString() + " scopes received.")
49 | foreach ($scope in $scopes) {
50 | Write-Verbose("Getting orgs with access to scope '$scope' ...")
51 | $orgs = . $ScopeAccess -env $Env -operator get -scope $scope | ForEach-Object { $_.consumer_orgno }
52 | $report[$scope] = $orgs
53 | }
54 | $report
55 | }
56 |
57 | function Generate-Extra-Report {
58 | $report = Generate-Full-Report
59 | $serviceOwners = @(Get-ServiceOwners -Env $env | select -ExpandProperty orgnr)
60 |
61 | $extraReport = @{}
62 | foreach ($scopeaccess in $report.GetEnumerator()) {
63 | $scope = $scopeaccess.Key;
64 | foreach ($org in $scopeaccess.Value) {
65 | Write-Verbose ("Checking if " + $org + " should have access to " + $scope);
66 | if (!$serviceOwners.Contains($org)) {
67 | Write-Verbose ($org + " is NOT service owner");
68 | if (!$extraReport.ContainsKey($org)) {
69 | $extraReport[$org] = @();
70 | }
71 | $extraReport[$org] += $scope;
72 | }
73 | else {
74 | Write-Verbose ($org + " is service owner")
75 | }
76 | }
77 | }
78 |
79 | $extraReport
80 | }
81 |
82 | function Generate-Missing-Report {
83 | $report = Generate-Full-Report
84 | $serviceOwners = Get-ServiceOwners -Env $env
85 |
86 | $missingReport = @{}
87 | foreach ($so in $serviceOwners) {
88 | $missing = @()
89 | Write-Verbose $so
90 | foreach ($scopeaccess in $report.GetEnumerator()) {
91 | if ($null -ne $scopeaccess.Value -and $scopeaccess.Value.Contains($so.orgnr)) {
92 | Write-Verbose ($so.name.en + " (" + $so.orgnr + ") has access to " + $scopeaccess.Key)
93 | } else {
94 | Write-Verbose ($so.name.en + " (" + $so.orgnr + ") MISSING access to " + $scopeaccess.Key)
95 | $missing += $scopeaccess.Key
96 | }
97 | }
98 | if ($missing.Count) {
99 | $missingReport[$so] = $missing
100 | }
101 | }
102 |
103 | $missingReport
104 | }
105 |
106 | function Add-Missing {
107 | $missingReport = Generate-Missing-Report
108 |
109 | foreach ($missing in $missingReport.GetEnumerator()) {
110 | foreach ($scope in $missing.Value) {
111 | Write-Output ("Giving " + $missing.Key.name.en + " (" + $missing.Key.orgnr + ") access to " + $scope)
112 | . $ScopeAccess -env $Env -operator add -scope $scope -org $missing.Key.orgnr
113 | }
114 | }
115 | }
116 |
117 | function Remove-Single {
118 | $report = Generate-Full-Report;
119 |
120 | foreach ($sa in $report.GetEnumerator()) {
121 | $scope = $sa.Key;
122 | Write-Output ("Removing " + $Org + " access to " + $scope)
123 | . $ScopeAccess -env $Env -operator remove -scope $scope -org $Org
124 | }
125 | }
126 |
127 | function Add-Single {
128 | $report = Generate-Full-Report;
129 |
130 | foreach ($sa in $report.GetEnumerator()) {
131 | $scope = $sa.Key;
132 | Write-Output ("Adding " + $Org + " access to " + $scope)
133 | . $ScopeAccess -env $Env -operator add -scope $scope -org $Org
134 | }
135 | }
136 |
137 | function Show-Missing {
138 | $missingReport = Generate-Missing-Report
139 | if (!$missingReport.Keys.Count) {
140 | Write-Output "No service owners are found missing scope access"
141 | }
142 | else {
143 | $missingList = @()
144 | foreach ($missing in $missingReport.GetEnumerator()) {
145 | foreach ($scope in $missing.Value) {
146 | Write-Verbose ("Org " + $missing.Key.name.en + " (" + $missing.Key.orgnr + ") missing access to " + $scope)
147 | $missingList += [PSCustomObject]@{ "Org" = $missing.Key.orgnr + " (" + $missing.Key.name.en + ")"; "Scope" = $scope }
148 | }
149 | }
150 | $missingList | Sort-Object -Property Org | Format-Table
151 | }
152 | }
153 |
154 |
155 | if ($Report) {
156 | Generate-Full-Report
157 | }
158 | elseif ($ShowMissing) {
159 | Show-Missing
160 | }
161 | elseif ($ShowExtra) {
162 | Generate-Extra-Report
163 | }
164 | elseif ($AddMissing) {
165 | Add-Missing
166 | }
167 | elseif ($RemoveExtra) {
168 | Remove-Extra
169 | }
170 | elseif ($RemoveSingle) {
171 | if ($Org -eq "") {
172 | Write-Warning "Must supply -Org"
173 | }
174 | else {
175 | Remove-Single
176 | }
177 | }
178 | elseif ($AddSingle) {
179 | if ($Org -eq "") {
180 | Write-Warning "Must supply -Org"
181 | }
182 | else {
183 | Add-Single
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/MaskinportenTokenGenerator/Server.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Text;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace MaskinportenTokenGenerator
7 | {
8 | class Server
9 | {
10 | private HttpListener _listener;
11 | private int _port;
12 | private string _codeVerifier;
13 | private TokenHandler _tokenHandler;
14 | private static string _cachedToken = null;
15 | private static DateTime _cachedTokenTtl = DateTime.Now;
16 | private string _authCode = null;
17 | private string _clientId = null;
18 | private string _redirectUri = null;
19 |
20 | public Server(TokenHandler token, int port, string clientId = null, string redirectUri = null, string codeVerifier = null)
21 | {
22 | _tokenHandler = token;
23 | _port = port;
24 | _codeVerifier = codeVerifier;
25 | _clientId = clientId;
26 | _redirectUri = redirectUri;
27 | }
28 |
29 | public void Listen()
30 | {
31 | _listener = new HttpListener();
32 | _listener.Prefixes.Add("http://localhost:" + _port.ToString() + "/");
33 | _listener.Start();
34 |
35 | while (true)
36 | {
37 | try
38 | {
39 | if (_listener.IsListening) {
40 | HttpListenerContext context = _listener.GetContext();
41 | Route(context);
42 | }
43 | else
44 | {
45 | break;
46 | }
47 | }
48 | catch (Exception ex)
49 | {
50 | Console.WriteLine("Exception caught: " + ex.GetType() + ": " + ex.Message);
51 | // Environment.Exit(1);
52 | }
53 | }
54 | // ReSharper disable once FunctionNeverReturns
55 | }
56 |
57 | private void Route(HttpListenerContext context)
58 | {
59 |
60 | switch (context.Request.Url.AbsolutePath)
61 | {
62 | case "/":
63 | ProcessTokenRequest(context);
64 | break;
65 |
66 | case "/response":
67 | ProcessAuthorizeResponse(context);
68 | break;
69 |
70 | default:
71 | context.Response.StatusCode = (int) HttpStatusCode.NotFound;
72 | context.Response.OutputStream.Close();
73 | break;
74 | }
75 | }
76 |
77 | private void ProcessTokenRequest(HttpListenerContext context)
78 | {
79 | string cacheQueryString = context.Request.QueryString["cache"];
80 | bool useCache = cacheQueryString != null && (cacheQueryString == "1" || cacheQueryString.ToLower() == "true");
81 | string accessToken;
82 | string assertion;
83 | bool isError;
84 | bool cacheHit = false;
85 |
86 | if (!useCache || _cachedToken == null || _cachedTokenTtl < DateTime.Now) {
87 | //Console.WriteLine("Cache stale or disabled, fetching new token");
88 | assertion = _tokenHandler.GetJwtAssertion();
89 | if (_authCode != null)
90 | {
91 | accessToken = _tokenHandler.GetTokenFromAuthCodeGrant(assertion, _authCode, _clientId, _redirectUri, _codeVerifier, out isError);
92 | }
93 | else
94 | {
95 | accessToken = _tokenHandler.GetTokenFromJwtBearerGrant(assertion, out isError);
96 | }
97 | }
98 | else {
99 | isError = false;
100 | accessToken = _cachedToken;
101 | cacheHit = true;
102 | //Console.WriteLine("Using cached token (expires at " + _cachedTokenTtl.ToString() + ")");
103 | }
104 |
105 | if (isError)
106 | {
107 | context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
108 | Console.WriteLine("################");
109 | Console.WriteLine("500 Internal Server error: Failed getting token");
110 | Console.WriteLine("Response from token endpoint was:");
111 | Console.WriteLine("---------------");
112 | Console.WriteLine(accessToken);
113 | Console.WriteLine("---------------");
114 | Console.WriteLine("Call made (formatted as curl command):");
115 | Console.WriteLine(_tokenHandler.CurlDebugCommand);
116 | if (_tokenHandler.LastException != null) TokenHandler.PrettyPrintException(_tokenHandler.LastException);
117 | if (_tokenHandler.LastTokenRequest != null) {
118 | Console.WriteLine("Token Request:");
119 | Console.WriteLine("---------------");
120 | Console.WriteLine(_tokenHandler.LastTokenRequest.Replace('&','\n'));
121 | Console.WriteLine("---------------");
122 | }
123 | }
124 | else {
125 | context.Response.ContentType = "application/json";
126 | context.Response.ContentLength64 = accessToken.Length;
127 | context.Response.AddHeader("Cache-Control", "no-cache");
128 | context.Response.AddHeader("X-TokenRequest", _tokenHandler.LastTokenRequest);
129 |
130 | var bytes = Encoding.UTF8.GetBytes(accessToken);
131 | context.Response.OutputStream.Write(bytes, 0, bytes.Length);
132 | context.Response.StatusCode = (int) HttpStatusCode.OK;
133 |
134 | if (useCache && !cacheHit)
135 | {
136 | dynamic response = JObject.Parse(accessToken);
137 | _cachedTokenTtl = DateTime.Now.AddSeconds((double)response.expires_in);
138 | _cachedToken = accessToken;
139 | //Console.WriteLine("Saving token to cache (expires at " + _cachedTokenTtl.ToString() + ")");
140 | }
141 | }
142 | //Console.WriteLine("----- COMPLETE -----");
143 |
144 | context.Response.OutputStream.Close();
145 | }
146 |
147 | private void ProcessAuthorizeResponse(HttpListenerContext context)
148 | {
149 | string code = context.Request.QueryString["code"];
150 |
151 | if (code == null)
152 | {
153 | context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
154 | }
155 | else
156 | {
157 | _authCode = code;
158 | context.Response.AddHeader("Cache-Control", "no-cache");
159 | context.Response.StatusCode = (int) HttpStatusCode.Redirect;
160 | context.Response.AddHeader("Location", "/?cache=true");
161 | }
162 |
163 | context.Response.OutputStream.Close();
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Project specific
3 |
4 | **/*config.local*
5 | scripts/serviceowners.local.json
6 | scripts/scopes.local.json
7 | scripts/exported
8 | scripts/imported
9 | secrets
10 | .DS_Store
11 | .idea
12 |
13 | ### VisualStudio ###
14 | ## Ignore Visual Studio temporary files, build results, and
15 | ## files generated by popular Visual Studio add-ons.
16 | ##
17 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
18 |
19 | # User-specific files
20 | *.rsuser
21 | *.suo
22 | *.user
23 | *.userosscache
24 | *.sln.docstates
25 |
26 | # User-specific files (MonoDevelop/Xamarin Studio)
27 | *.userprefs
28 |
29 | # Mono auto generated files
30 | mono_crash.*
31 |
32 | # Build results
33 | [Dd]ebug/
34 | [Dd]ebugPublic/
35 | [Rr]elease/
36 | [Rr]eleases/
37 | x64/
38 | x86/
39 | [Aa][Rr][Mm]/
40 | [Aa][Rr][Mm]64/
41 | bld/
42 | [Bb]in/
43 | [Oo]bj/
44 | [Ll]og/
45 |
46 | # Visual Studio 2015/2017 cache/options directory
47 | .vs/
48 | # Uncomment if you have tasks that create the project's static files in wwwroot
49 | #wwwroot/
50 |
51 | # Visual Studio 2017 auto generated files
52 | Generated\ Files/
53 |
54 | # MSTest test Results
55 | [Tt]est[Rr]esult*/
56 | [Bb]uild[Ll]og.*
57 |
58 | # NUnit
59 | *.VisualState.xml
60 | TestResult.xml
61 | nunit-*.xml
62 |
63 | # Build Results of an ATL Project
64 | [Dd]ebugPS/
65 | [Rr]eleasePS/
66 | dlldata.c
67 |
68 | # Benchmark Results
69 | BenchmarkDotNet.Artifacts/
70 |
71 | # .NET Core
72 | project.lock.json
73 | project.fragment.lock.json
74 | artifacts/
75 |
76 | # StyleCop
77 | StyleCopReport.xml
78 |
79 | # Files built by Visual Studio
80 | *_i.c
81 | *_p.c
82 | *_h.h
83 | *.ilk
84 | *.meta
85 | *.obj
86 | *.iobj
87 | *.pch
88 | *.pdb
89 | *.ipdb
90 | *.pgc
91 | *.pgd
92 | *.rsp
93 | *.sbr
94 | *.tlb
95 | *.tli
96 | *.tlh
97 | *.tmp
98 | *.tmp_proj
99 | *_wpftmp.csproj
100 | *.log
101 | *.vspscc
102 | *.vssscc
103 | .builds
104 | *.pidb
105 | *.svclog
106 | *.scc
107 |
108 | # Chutzpah Test files
109 | _Chutzpah*
110 |
111 | # Visual C++ cache files
112 | ipch/
113 | *.aps
114 | *.ncb
115 | *.opendb
116 | *.opensdf
117 | *.sdf
118 | *.cachefile
119 | *.VC.db
120 | *.VC.VC.opendb
121 |
122 | # Visual Studio profiler
123 | *.psess
124 | *.vsp
125 | *.vspx
126 | *.sap
127 |
128 | # Visual Studio Trace Files
129 | *.e2e
130 |
131 | # TFS 2012 Local Workspace
132 | $tf/
133 |
134 | # Guidance Automation Toolkit
135 | *.gpState
136 |
137 | # ReSharper is a .NET coding add-in
138 | _ReSharper*/
139 | *.[Rr]e[Ss]harper
140 | *.DotSettings.user
141 |
142 | # JustCode is a .NET coding add-in
143 | .JustCode
144 |
145 | # TeamCity is a build add-in
146 | _TeamCity*
147 |
148 | # DotCover is a Code Coverage Tool
149 | *.dotCover
150 |
151 | # AxoCover is a Code Coverage Tool
152 | .axoCover/*
153 | !.axoCover/settings.json
154 |
155 | # Visual Studio code coverage results
156 | *.coverage
157 | *.coveragexml
158 |
159 | # NCrunch
160 | _NCrunch_*
161 | .*crunch*.local.xml
162 | nCrunchTemp_*
163 |
164 | # MightyMoose
165 | *.mm.*
166 | AutoTest.Net/
167 |
168 | # Web workbench (sass)
169 | .sass-cache/
170 |
171 | # Installshield output folder
172 | [Ee]xpress/
173 |
174 | # DocProject is a documentation generator add-in
175 | DocProject/buildhelp/
176 | DocProject/Help/*.HxT
177 | DocProject/Help/*.HxC
178 | DocProject/Help/*.hhc
179 | DocProject/Help/*.hhk
180 | DocProject/Help/*.hhp
181 | DocProject/Help/Html2
182 | DocProject/Help/html
183 |
184 | # Click-Once directory
185 | publish/
186 |
187 | # Publish Web Output
188 | *.[Pp]ublish.xml
189 | *.azurePubxml
190 | # Note: Comment the next line if you want to checkin your web deploy settings,
191 | # but database connection strings (with potential passwords) will be unencrypted
192 | *.pubxml
193 | *.publishproj
194 |
195 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
196 | # checkin your Azure Web App publish settings, but sensitive information contained
197 | # in these scripts will be unencrypted
198 | PublishScripts/
199 |
200 | # NuGet Packages
201 | *.nupkg
202 | # NuGet Symbol Packages
203 | *.snupkg
204 | # The packages folder can be ignored because of Package Restore
205 | **/[Pp]ackages/*
206 | # except build/, which is used as an MSBuild target.
207 | !**/[Pp]ackages/build/
208 | # Uncomment if necessary however generally it will be regenerated when needed
209 | #!**/[Pp]ackages/repositories.config
210 | # NuGet v3's project.json files produces more ignorable files
211 | *.nuget.props
212 | *.nuget.targets
213 |
214 | # Microsoft Azure Build Output
215 | csx/
216 | *.build.csdef
217 |
218 | # Microsoft Azure Emulator
219 | ecf/
220 | rcf/
221 |
222 | # Windows Store app package directories and files
223 | AppPackages/
224 | BundleArtifacts/
225 | Package.StoreAssociation.xml
226 | _pkginfo.txt
227 | *.appx
228 | *.appxbundle
229 | *.appxupload
230 |
231 | # Visual Studio cache files
232 | # files ending in .cache can be ignored
233 | *.[Cc]ache
234 | # but keep track of directories ending in .cache
235 | !?*.[Cc]ache/
236 |
237 | # Others
238 | ClientBin/
239 | ~$*
240 | *~
241 | *.dbmdl
242 | *.dbproj.schemaview
243 | *.jfm
244 | *.pfx
245 | *.publishsettings
246 | orleans.codegen.cs
247 |
248 | # Including strong name files can present a security risk
249 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
250 | #*.snk
251 |
252 | # Since there are multiple workflows, uncomment next line to ignore bower_components
253 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
254 | #bower_components/
255 |
256 | # RIA/Silverlight projects
257 | Generated_Code/
258 |
259 | # Backup & report files from converting an old project file
260 | # to a newer Visual Studio version. Backup files are not needed,
261 | # because we have git ;-)
262 | _UpgradeReport_Files/
263 | Backup*/
264 | UpgradeLog*.XML
265 | UpgradeLog*.htm
266 | ServiceFabricBackup/
267 | *.rptproj.bak
268 |
269 | # SQL Server files
270 | *.mdf
271 | *.ldf
272 | *.ndf
273 |
274 | # Business Intelligence projects
275 | *.rdl.data
276 | *.bim.layout
277 | *.bim_*.settings
278 | *.rptproj.rsuser
279 | *- [Bb]ackup.rdl
280 | *- [Bb]ackup ([0-9]).rdl
281 | *- [Bb]ackup ([0-9][0-9]).rdl
282 |
283 | # Microsoft Fakes
284 | FakesAssemblies/
285 |
286 | # GhostDoc plugin setting file
287 | *.GhostDoc.xml
288 |
289 | # Node.js Tools for Visual Studio
290 | .ntvs_analysis.dat
291 | node_modules/
292 |
293 | # Visual Studio 6 build log
294 | *.plg
295 |
296 | # Visual Studio 6 workspace options file
297 | *.opt
298 |
299 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
300 | *.vbw
301 |
302 | # Visual Studio LightSwitch build output
303 | **/*.HTMLClient/GeneratedArtifacts
304 | **/*.DesktopClient/GeneratedArtifacts
305 | **/*.DesktopClient/ModelManifest.xml
306 | **/*.Server/GeneratedArtifacts
307 | **/*.Server/ModelManifest.xml
308 | _Pvt_Extensions
309 |
310 | # Paket dependency manager
311 | .paket/paket.exe
312 | paket-files/
313 |
314 | # FAKE - F# Make
315 | .fake/
316 |
317 | # CodeRush personal settings
318 | .cr/personal
319 |
320 | # Python Tools for Visual Studio (PTVS)
321 | __pycache__/
322 | *.pyc
323 |
324 | # Cake - Uncomment if you are using it
325 | # tools/**
326 | # !tools/packages.config
327 |
328 | # Tabs Studio
329 | *.tss
330 |
331 | # Telerik's JustMock configuration file
332 | *.jmconfig
333 |
334 | # BizTalk build output
335 | *.btp.cs
336 | *.btm.cs
337 | *.odx.cs
338 | *.xsd.cs
339 |
340 | # OpenCover UI analysis results
341 | OpenCover/
342 |
343 | # Azure Stream Analytics local run output
344 | ASALocalRun/
345 |
346 | # MSBuild Binary and Structured Log
347 | *.binlog
348 |
349 | # NVidia Nsight GPU debugger configuration file
350 | *.nvuser
351 |
352 | # MFractors (Xamarin productivity tool) working folder
353 | .mfractor/
354 |
355 | # Local History for Visual Studio
356 | .localhistory/
357 |
358 | # BeatPulse healthcheck temp database
359 | healthchecksdb
360 |
361 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
362 | MigrationBackup/
363 |
364 | # End of https://www.gitignore.io/api/visualstudio
365 |
--------------------------------------------------------------------------------
/scripts/scope.ps1:
--------------------------------------------------------------------------------
1 | # Script to administrate scopes defined in ID/Maskinporten
2 | # Author: bdl@digdir.no
3 | # -----------------------------------------------------------------------------------------------------------------
4 | # NOTE! Requires a "scopes-admin.config.local.ps1" file present in same directory as script.
5 | # -----------------------------------------------------------------------------------------------------------------
6 | #
7 | # Examples:
8 | # ./scope get -prefix altinn
9 | # ./scope get -scope altinn:foo
10 | # ./scope new -file definition.json
11 | # ./scope new -definition $definition
12 | # ./scope update -file definition.json
13 | # ./scope update -definition $definition
14 | # ./scope update -definition $definition
15 | # ./scope export-to-csv -prefix altinn
16 | # ./scope import-from-csv -file somefile.csv
17 | # ./scope export-to-json -prefix altinn
18 | #
19 | # Environment defaults to "test". Can be overridden by supplying a -env parameter containing "test" or "prod"
20 | #
21 |
22 | param (
23 | [Parameter(Mandatory=$true)][string]$operator,
24 | [Parameter()][string]$scope,
25 | [Parameter()][string]$file,
26 | [Parameter()][string]$definition,
27 | [Parameter()][string]$prefix,
28 | [Parameter()][string]$env = "test"
29 | )
30 |
31 | . ($PSScriptRoot + "/config.ps1")
32 | . ($PSScriptRoot + "/token.ps1")
33 | . ($PSScriptRoot + "/env.ps1")
34 |
35 | function Get-Scope {
36 | param($scope, $prefix)
37 | if ($scope -ne "") {
38 | $result = Invoke-API -Verb GET -Path "/scopes?scope=$scope"
39 | }
40 | else {
41 | $result = Invoke-API -Verb GET -Path "/scopes"
42 | }
43 |
44 | if ($null -eq $result) {
45 | Write-Output "No scope found"
46 | }
47 | elseif ($prefix -ne "") {
48 | $result | Where-Object { $_.prefix -eq $prefix }
49 | }
50 | else {
51 | $result
52 | }
53 | }
54 |
55 | function New-Scope-From-File {
56 | param($file)
57 | if (!(Test-Path $file)) {
58 | Write-Error "$file not found"
59 | Exit 1
60 | }
61 | $JsonBody = Get-Content $file
62 | Write-Verbose($JsonBody | ConvertFrom-Json | Format-List | Out-String)
63 | Invoke-API -Verb POST -Path "/scopes" -Body $JsonBody
64 | }
65 |
66 | function New-Scope-From-Definition {
67 | param($definition)
68 | $JsonBody = $definition | ConvertTo-Json
69 | Write-Verbose($JsonBody | ConvertFrom-Json | Format-List | Out-String)
70 | Invoke-API -Verb POST -Path "/scopes" -Body $JsonBody
71 | }
72 |
73 | function Update-Scope-From-File {
74 | param($file)
75 | if (!(Test-Path $file)) {
76 | Write-Error "$file not found"
77 | Exit 1
78 | }
79 | $JsonBody = Get-Content $file
80 | $scope = ($JsonBody | ConvertFrom-Json).name
81 | Write-Verbose($JsonBody | ConvertFrom-Json | Format-List | Out-String)
82 | $result = Invoke-API -Verb PUT -Path "/scopes?scope=$scope" -Body $JsonBody
83 | if ($null -ne $result.name) {
84 | Write-Output ("Updated " + $result.name)
85 | }
86 | else {
87 | Write-Warning "Failed updating from file: $file"
88 | }
89 | }
90 |
91 | function Update-Scope-From-Definition {
92 | param($definition)
93 | $scope = $definition.name
94 | $JsonBody = $definition | ConvertTo-Json
95 | Write-Verbose($JsonBody | ConvertFrom-Json | Format-List | Out-String)
96 | $result = Invoke-API -Verb PUT -Path "/scopes?scope=$scope" -Body $JsonBody
97 | if ($null -ne $result.name) {
98 | Write-Output ("Updated " + $result.name)
99 | }
100 | else {
101 | Write-Warning "Failed updating from definition:"
102 | $definition
103 | }
104 | }
105 |
106 | function Export-To-JSONs {
107 | param($prefix)
108 | $scopes = Get-Scope "" $prefix
109 | Convert-Scopes-To-Json-Files $scopes "exported"
110 | }
111 |
112 | function Convert-Scopes-To-Json-Files {
113 | param($scopes, $dir)
114 | if (!(Test-Path $dir)) {
115 | New-Item -ItemType Directory -Force -Path $dir | Out-Null
116 | }
117 | $scopes | Select-Object -Property * -ExcludeProperty created,last_updated | ForEach-Object {
118 | $fullpath = "$dir/scopes/$($_.prefix)/$($_.subscope).json";
119 | $parts = $fullpath.Split("/");
120 | $path = $parts | Select-Object -skiplast 1;
121 | $path = $path -join "/"
122 |
123 | New-Item -Name $path -ItemType "directory" -ErrorAction Ignore | Out-Null
124 |
125 | Write-Verbose "Exporting to $fullpath ..."
126 |
127 | $_ | ConvertTo-Json | Out-File -FilePath $fullpath
128 | }
129 | }
130 |
131 | function Export-To-CSV {
132 | param($prefix);
133 | $scopes = Get-Scope "" $prefix
134 | if (!(Test-Path "exported")) {
135 | New-Item -ItemType Directory -Force -Path "exported" | Out-Null
136 | }
137 | $filename = "exported/export-" + $env + "-"
138 | if ($prefix -ne "") {
139 | $filename += $prefix + "-"
140 | }
141 | $filename += (Get-Date -Format "yyyy-MM-dd_HHmmss") + ".csv"
142 | $scopes = ProcessToCsv $scopes
143 | $scopes | Select-Object -Property * -ExcludeProperty created,last_updated | Export-Csv -Path .\$filename -NoTypeInformation -Encoding unicode
144 | Write-Output "Exported to $filename."
145 | }
146 |
147 | function Import-From-CSV {
148 | param($file)
149 | $scopes = Get-Content -Raw $file | ConvertFrom-Csv
150 | $scopes = ProcessFromCsv $scopes
151 | Convert-Scopes-To-Json-Files $scopes "imported"
152 | }
153 |
154 | function ProcessToCsv {
155 | param($scopes)
156 | $newscopes = [System.Collections.ArrayList]::new();
157 | $scopes | ForEach-Object {
158 | # We cannot handle arrays in CSV
159 | $_.allowed_integration_types = [system.String]::Join(",", $_.allowed_integration_types);
160 |
161 | # Some fields might be omitted, always include even if not supplied
162 | if (!("delegation_source" -in $_.PSobject.Properties.Name)) {
163 | $_ | Add-Member -NotePropertyName "delegation_source" -NotePropertyValue ""
164 | }
165 | if (!("authorization_max_lifetime" -in $_.PSobject.Properties.Name)) {
166 | $_ | Add-Member -NotePropertyName "authorization_max_lifetime" -NotePropertyValue ""
167 | }
168 | if (!("at_max_age" -in $_.PSobject.Properties.Name)) {
169 | $_ | Add-Member -NotePropertyName "at_max_age" -NotePropertyValue ""
170 | }
171 | if (!("long_description" -in $_.PSobject.Properties.Name)) {
172 | $_ | Add-Member -NotePropertyName "long_description" -NotePropertyValue ""
173 | }
174 |
175 | [void]$newscopes.Add($_);
176 | }
177 | $newscopes
178 | }
179 |
180 | # This effectively does the reverse of ProcessToCsv
181 | function ProcessFromCsv {
182 | param($scopes)
183 | $newscopes = [System.Collections.ArrayList]::new();
184 | $scopes | ForEach-Object {
185 | # Convert from comma separated list to array
186 | $_.allowed_integration_types = $_.allowed_integration_types.Split(",");
187 |
188 | # Drop fields without value
189 | if ($_.delegation_source -eq "") {
190 | $_ = $_ | Select-Object -Property * -ExcludeProperty delegation_source
191 | }
192 | if ($_.authorization_max_lifetime -eq "") {
193 | $_ = $_ | Select-Object -Property * -ExcludeProperty authorization_max_lifetime
194 | }
195 | if ($_.at_max_age -eq "") {
196 | $_ = $_ | Select-Object -Property * -ExcludeProperty at_max_age
197 | }
198 | if ($_.long_description -eq "" -or $_.long_description -eq $null) {
199 | $_ = $_ | Select-Object -Property * -ExcludeProperty long_description
200 | }
201 | else {
202 | # Replace \\n with \n
203 | $_.long_description = $_.long_description.Replace('\\n','\n')
204 | }
205 |
206 | [void]$newscopes.Add($_);
207 | }
208 | $newscopes
209 | }
210 |
211 | #####################################################
212 |
213 | if ($operator -eq 'get') {
214 | Get-Scope $scope $prefix
215 | }
216 | elseif ($operator -eq 'export-to-json') {
217 | Export-To-JSONs $prefix
218 | }
219 | elseif ($operator -eq 'export-to-csv') {
220 | Export-To-CSV $prefix
221 | }
222 | elseif ($operator -eq 'import-from-csv') {
223 | Import-From-CSV $file
224 | }
225 | elseif ($operator -eq 'new') {
226 | if ($file -ne $null) {
227 | New-Scope-From-File $file
228 | }
229 | elseif ($definition -ne $null) {
230 | New-Scope-From-Definition $definition
231 | }
232 | }
233 | elseif ($operator -eq 'update') {
234 | if ($file -ne $null) {
235 | Update-Scope-From-File $file
236 | }
237 | elseif ($definition -ne $null) {
238 | Update-Scope-From-Definition $definition
239 | }
240 | }
--------------------------------------------------------------------------------
/src/MaskinportenTokenGenerator/TokenHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IdentityModel.Tokens.Jwt;
4 | using System.IO;
5 | using System.Net;
6 | using System.Net.Http;
7 | using System.Security.Cryptography.X509Certificates;
8 | using Microsoft.IdentityModel.Tokens;
9 |
10 | namespace MaskinportenTokenGenerator
11 | {
12 | public class TokenHandler
13 | {
14 |
15 | private readonly string _issuer;
16 | private readonly string _audience;
17 | private readonly string _resource;
18 | private readonly string _scopes;
19 | private readonly string _tokenEndpoint;
20 | private readonly int _tokenTtl;
21 | private readonly X509Certificate2 _signingCertificate;
22 | private readonly SecurityKey _signingKey;
23 | private readonly string _kidClaim;
24 | private readonly string _consumerOrg;
25 |
26 | public string LastTokenRequest { get; private set; }
27 | public Exception LastException { get; private set; }
28 | public string CurlDebugCommand { get; private set; }
29 |
30 | public TokenHandler(string certificateThumbprint, StoreLocation certificateStoreLocation, string kidClaim, string tokenEndpoint, string audience, string resource,
31 | string scopes, string issuer, int tokenTtl, string consumerOrg)
32 | {
33 | _signingCertificate = GetCertificateFromKeyStore(certificateThumbprint, StoreName.My, certificateStoreLocation);
34 |
35 | _kidClaim = kidClaim;
36 | _tokenEndpoint = tokenEndpoint;
37 | _audience = audience;
38 | _resource = resource;
39 | _scopes = scopes;
40 | _issuer = issuer;
41 | _tokenTtl = tokenTtl;
42 | _consumerOrg = consumerOrg;
43 | }
44 |
45 | public TokenHandler(string p12KeyStoreFile, string p12KeyStorePassword, string kidClaim, string tokenEndpoint, string audience, string resource,
46 | string scopes, string issuer, int tokenTtl, string consumerOrg)
47 | {
48 | _signingCertificate = new X509Certificate2(
49 | File.ReadAllBytes(p12KeyStoreFile),
50 | p12KeyStorePassword,
51 | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
52 |
53 | _kidClaim = kidClaim;
54 | _tokenEndpoint = tokenEndpoint;
55 | _audience = audience;
56 | _resource = resource;
57 | _scopes = scopes;
58 | _issuer = issuer;
59 | _tokenTtl = tokenTtl;
60 | _consumerOrg = consumerOrg;
61 | }
62 |
63 | public TokenHandler(string jwkJsonFile, bool isKeySetFormat, string kidClaim, string tokenEndpoint, string audience, string resource,
64 | string scopes, string issuer, int tokenTtl, string consumerOrg)
65 | {
66 | if (isKeySetFormat)
67 | {
68 | throw new NotImplementedException();
69 | }
70 | else
71 | {
72 | _signingKey = new JsonWebKey(File.ReadAllText(jwkJsonFile));
73 | }
74 |
75 | _kidClaim = kidClaim;
76 | _tokenEndpoint = tokenEndpoint;
77 | _audience = audience;
78 | _resource = resource;
79 | _scopes = scopes;
80 | _issuer = issuer;
81 | _tokenTtl = tokenTtl;
82 | _consumerOrg = consumerOrg;
83 | }
84 |
85 | public string GetTokenFromAuthCodeGrant(string assertion, string code, string clientId, string redirectUri, string codeVerifier, out bool isError)
86 | {
87 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
88 |
89 | var formContent = new FormUrlEncodedContent(new List>
90 | {
91 | new KeyValuePair("client_id", clientId),
92 | new KeyValuePair("grant_type", "authorization_code"),
93 | new KeyValuePair("code", code),
94 | new KeyValuePair("redirect_uri", redirectUri),
95 | new KeyValuePair("code_verifier", codeVerifier),
96 | new KeyValuePair("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
97 | new KeyValuePair("client_assertion", assertion)
98 | });
99 |
100 | LastTokenRequest = formContent.ReadAsStringAsync().Result;
101 | return SendTokenRequest(formContent, out isError);
102 | }
103 |
104 | public string GetTokenFromJwtBearerGrant(string assertion, out bool isError)
105 | {
106 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
107 |
108 | var formContent = new FormUrlEncodedContent(new List>
109 | {
110 | new KeyValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
111 | new KeyValuePair("assertion", assertion),
112 | });
113 |
114 | LastTokenRequest = formContent.ReadAsStringAsync().Result;
115 | return SendTokenRequest(formContent, out isError);
116 | }
117 |
118 | public static void PrettyPrintException(Exception e)
119 | {
120 | Console.WriteLine("############");
121 | Console.WriteLine("Failed request to token endpoint, Exception thrown: " + e.GetType().FullName);
122 | Console.WriteLine("Message:" + e.Message);
123 | Console.WriteLine("Stack trace:");
124 | Console.WriteLine(e.StackTrace);
125 | while (e.InnerException != null)
126 | {
127 | Console.WriteLine("Inner Exception:" + e.InnerException.GetType().FullName);
128 | Console.WriteLine("Message:" + e.InnerException.Message);
129 | Console.WriteLine("Stack trace:");
130 | Console.WriteLine(e.InnerException.StackTrace);
131 | e = e.InnerException;
132 | }
133 |
134 | Console.WriteLine("############");
135 | }
136 |
137 | public string GetJwtAssertion()
138 | {
139 | var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow);
140 | JwtHeader header;
141 | if (_signingCertificate != null)
142 | {
143 | var securityKey = new X509SecurityKey(_signingCertificate);
144 | header = new JwtHeader(new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256))
145 | {
146 | { "x5c", new List() { Convert.ToBase64String(_signingCertificate.GetRawCertData()) } }
147 | };
148 | header.Remove("typ");
149 | }
150 | else if (_signingKey != null)
151 | {
152 | // TODO! We always assume RS256
153 | header = new JwtHeader(new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256));
154 | header.Remove("typ");
155 | }
156 | else
157 | {
158 | throw new ArgumentException(
159 | "Internal error: expected either _signingCertificate or _signingKey to be non-null");
160 | }
161 |
162 | // kid claim by default is set to x5t (certificate thumbprint). This can only be supplied if
163 | // the client is configured with a custom public key, and must be removed if signing the assertion
164 | // with a enterprise certificate. For convenience, the magic value "thumbprint" allows the
165 | // kid to stay the same as certificate thumbprint
166 | if (_kidClaim != null && _kidClaim != "thumbprint")
167 | {
168 | header.Remove("kid");
169 | header.Add("kid", _kidClaim);
170 | }
171 | else if (_kidClaim == null)
172 | {
173 | header.Remove("kid");
174 | }
175 |
176 | var payload = new JwtPayload
177 | {
178 | { "aud", _audience },
179 | { "scope", _scopes },
180 | { "sub", _issuer }, // See https://docs.digdir.no/docs/idporten/oidc/oidc_protocol_token.html#client-authentication-using-jwt-token
181 | { "iss", _issuer },
182 | { "exp", dateTimeOffset.ToUnixTimeSeconds() + _tokenTtl },
183 | { "iat", dateTimeOffset.ToUnixTimeSeconds() },
184 | { "jti", Guid.NewGuid().ToString() },
185 | };
186 |
187 | if (_resource != null)
188 | {
189 | payload.Add("resource", _resource);
190 | }
191 |
192 | if (_consumerOrg != null) {
193 | payload.Add("consumer_org", _consumerOrg);
194 | }
195 |
196 | var securityToken = new JwtSecurityToken(header, payload);
197 | var handler = new JwtSecurityTokenHandler();
198 |
199 | return handler.WriteToken(securityToken);
200 | }
201 |
202 | private string SendTokenRequest(FormUrlEncodedContent formContent, out bool isError)
203 | {
204 | var client = new HttpClient();
205 |
206 | CurlDebugCommand = "curl -v -X POST -d '" + formContent.ReadAsStringAsync().Result + "' " + _tokenEndpoint;
207 | try {
208 | var response = client.PostAsync(_tokenEndpoint, formContent).Result;
209 | isError = !response.IsSuccessStatusCode;
210 | return response.Content.ReadAsStringAsync().Result;
211 | }
212 | catch (Exception e)
213 | {
214 | LastException = e;
215 | isError = true;
216 | PrettyPrintException(e);
217 | return null;
218 | }
219 | }
220 |
221 | private static X509Certificate2 GetCertificateFromKeyStore(string thumbprint, StoreName storeName, StoreLocation storeLocation, bool onlyValid = false)
222 | {
223 | var store = new X509Store(storeName, storeLocation);
224 | store.Open(OpenFlags.ReadOnly);
225 | var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, onlyValid);
226 | var enumerator = certCollection.GetEnumerator();
227 | X509Certificate2 cert = null;
228 | while (enumerator.MoveNext())
229 | {
230 | cert = enumerator.Current;
231 | }
232 |
233 | if (cert == null)
234 | {
235 | throw new ArgumentException("Unable to find certificate in store with thumbprint: " + thumbprint + ". Check your config, and make sure the certificate is installed in the \"LocalMachine\\My\" store.");
236 | }
237 |
238 | return cert;
239 | }
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/src/MaskinportenTokenGenerator/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Net;
4 | using System.Runtime.InteropServices;
5 | using System.Security.Cryptography;
6 | using System.Security.Cryptography.X509Certificates;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using Mono.Options;
10 | using Newtonsoft.Json;
11 | using Newtonsoft.Json.Linq;
12 |
13 | namespace MaskinportenTokenGenerator
14 | {
15 | internal class Program
16 | {
17 | private static string _certificateThumbPrint;
18 | private static string _p12KeyStoreFile;
19 | private static string _p12KeyStorePassword;
20 | private static string _jwkFile;
21 | private static string _kidClaim;
22 | private static string _issuer;
23 | private static string _audience;
24 | private static string _resource;
25 | private static string _scopes;
26 | private static string _tokenEndpoint;
27 | private static string _authorizeEndpoint;
28 | private static int _tokenTtl = 120;
29 | private static string _consumerOrg;
30 | private static string _codeVerifier;
31 |
32 | [STAThread]
33 | static void Main(string[] args)
34 | {
35 | var showHelp = false;
36 | var serverMode = false;
37 | var onlyToken = false;
38 | var onlyGrant = false;
39 | var serverPort = 17823;
40 | var personMode = false;
41 | var useCurrentUserStoreLocation = false;
42 |
43 | var p = new OptionSet() {
44 | { "t=|certificate_thumbprint=", "(Windows only) Thumbprint for certificate to use, see Cert:\\{LocalMachine,CurrentUser}\\My in Powershell.",
45 | v => _certificateThumbPrint = v },
46 | { "u=|use_current_user_store_location=", "(Windows only) User CurrentUser certificate store location (default: LocalMachine)",
47 | v => useCurrentUserStoreLocation = v != null && v == "true" },
48 | { "k=|keystore_path=", "Path to PKCS12 file containing certificate to use.",
49 | v => _p12KeyStoreFile = v },
50 | { "p=|keystore_password=", "Path to PKCS12 file containing certificate to use.",
51 | v => _p12KeyStorePassword = v },
52 | { "j=|jwk_path=", "Path to JSON file containing JWK with public/private key.",
53 | v => _jwkFile = v },
54 | { "K=|kid=", "Set kid-claim in bearer grant assertion header. Used for pre-registered JWK clients.",
55 | v => _kidClaim = v },
56 | { "c=|client_id=", "This is the client_id to which the access_token is requested",
57 | v => _issuer = v },
58 | { "a=|audience=", "The audience for the grant, must be ID-porten",
59 | v => _audience = v },
60 | { "r=|resource=", "Intended audience, used as aud-claim in returned access_token",
61 | v => _resource = v },
62 | { "l=|token_ttl=", "Token lifetime in seconds (default: 120)",
63 | v =>
64 | {
65 | if (v != null && Int32.TryParse(v, out int overriddenTokenTtl))
66 | {
67 | _tokenTtl = overriddenTokenTtl;
68 | }
69 | }
70 | },
71 | { "s=|scopes=", "Scopes requested, comma separated",
72 | v => _scopes = v.Replace(',', ' ') },
73 | { "e=|token_endpoint=", "Token endpoint to ask for access_token",
74 | v => _tokenEndpoint = v },
75 | { "A=|authorize_endpoint=", "Authorize endpoint to redirect user for consent",
76 | v => _authorizeEndpoint = v },
77 | { "C|consumer_org=", "Enable supplier mode for given consumer organization number",
78 | v => _consumerOrg = v },
79 | { "m|server_mode", "Enable server mode",
80 | v => serverMode = v != null },
81 | { "P=|server_port=", "Server port (default 17823)",
82 | v =>
83 | {
84 | if (v != null && UInt16.TryParse(v, out ushort overriddenServerPort))
85 | {
86 | serverPort = overriddenServerPort;
87 | }
88 | }
89 | },
90 | { "i=|person_mode=", "Enable person mode (ID-porten)",
91 | v => personMode = v != null && v == "true" },
92 | { "o|only_token", "Only return token to stdout",
93 | v => onlyToken = v != null },
94 | { "g|only_grant", "Only return bearer grant to stdout",
95 | v => onlyGrant = v != null },
96 | { "h|help", "show this message and exit",
97 | v => showHelp = v != null },
98 |
99 | };
100 |
101 | try
102 | {
103 | p.Parse(args);
104 | }
105 | catch (OptionException e)
106 | {
107 | Console.Write("MaskinportenTokenGenerator: ");
108 | Console.WriteLine(e.Message);
109 | Console.WriteLine("Try `MaskinportenTokenGenerator --help' for more information.");
110 | return;
111 | }
112 |
113 | if (showHelp)
114 | {
115 | ShowHelp(p);
116 | return;
117 | }
118 |
119 | CheckParameters(p);
120 |
121 | TokenHandler tokenHandler;
122 | try {
123 | if (_certificateThumbPrint != null) {
124 |
125 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
126 | {
127 | Console.WriteLine("Error: --certificate_thumbprint is only supported on Windows");
128 | Environment.Exit(1);
129 | }
130 |
131 | var storeLocation = useCurrentUserStoreLocation ? StoreLocation.CurrentUser : StoreLocation.LocalMachine;
132 | tokenHandler = new TokenHandler(_certificateThumbPrint, storeLocation, _kidClaim, _tokenEndpoint, _audience, _resource, _scopes, _issuer, _tokenTtl, _consumerOrg);
133 | }
134 | else if (_jwkFile != null)
135 | {
136 | tokenHandler = new TokenHandler(_jwkFile, false, _kidClaim, _tokenEndpoint, _audience, _resource, _scopes, _issuer, _tokenTtl, _consumerOrg);
137 | }
138 | else
139 | {
140 | tokenHandler = new TokenHandler(_p12KeyStoreFile, _p12KeyStorePassword, _kidClaim, _tokenEndpoint, _audience, _resource, _scopes, _issuer, _tokenTtl, _consumerOrg);
141 | }
142 | }
143 | catch (Exception e)
144 | {
145 | Console.WriteLine("Caught exception " + e.GetType().FullName + ": " + e.Message);
146 | Console.WriteLine();
147 | if (!onlyToken) {
148 | Console.WriteLine("Press ENTER to exit.");
149 | Console.ReadLine();
150 | }
151 | Environment.Exit(1);
152 | return; // To please code inspector complaining about token being undefined below
153 | }
154 |
155 | if (!serverMode && !personMode)
156 | {
157 | var assertion = tokenHandler.GetJwtAssertion();
158 | if (onlyGrant)
159 | {
160 | Console.WriteLine(assertion);
161 | Environment.Exit(0);
162 | }
163 |
164 | var token = tokenHandler.GetTokenFromJwtBearerGrant(assertion, out bool isError);
165 |
166 | if (isError)
167 | {
168 | Console.WriteLine("Failed getting token: " + token);
169 | Console.WriteLine("Call made (formatted as curl command):");
170 | Console.WriteLine(tokenHandler.CurlDebugCommand);
171 | }
172 | else
173 | {
174 | var tokenObject = JsonConvert.DeserializeObject(token);
175 | if (onlyToken)
176 | {
177 | Console.WriteLine(tokenObject.GetValue("access_token"));
178 | Environment.Exit(0);
179 | }
180 |
181 | Console.WriteLine("Got successful response:");
182 | Console.WriteLine("----------------------------------------");
183 | Console.WriteLine(token);
184 | Console.WriteLine("----------------------------------------");
185 | }
186 |
187 | if (!onlyToken) {
188 | Console.WriteLine("Press ENTER to exit.");
189 | Console.ReadLine();
190 | }
191 | Environment.Exit(isError ? 1 : 0);
192 | }
193 |
194 |
195 | Server server;
196 |
197 | if (personMode)
198 | {
199 | _codeVerifier = GenerateCodeVerifier();
200 | string url = GetAuthorizeUrl(serverPort, GeneratePkceChallenge(_codeVerifier));
201 | Console.WriteLine("Person login mode, opening browser to: " + url);
202 | System.Diagnostics.Process.Start(url);
203 | server = new Server(tokenHandler, serverPort, _issuer, GetRedirectUri(serverPort), _codeVerifier);
204 | }
205 | else
206 | {
207 | server = new Server(tokenHandler, serverPort);
208 | }
209 |
210 | Console.WriteLine("Server started, serving tokens at http://localhost:" + serverPort.ToString() + "/");
211 | Task.Run(() => server.Listen());
212 | Console.WriteLine("Press ESCAPE or CTRL-C to exit");
213 |
214 | while (true)
215 | {
216 | if (Console.KeyAvailable)
217 | {
218 | ConsoleKeyInfo info = Console.ReadKey(true);
219 | if (info.Key == ConsoleKey.Escape || info.Modifiers == ConsoleModifiers.Control && info.Key == ConsoleKey.C)
220 | {
221 | Console.WriteLine("Bye!");
222 | Environment.Exit(0);
223 | }
224 | }
225 | }
226 | }
227 |
228 | static void ShowHelp(OptionSet p)
229 | {
230 | Console.WriteLine("Usage: MaskinportenTokenGenerator [OPTIONS]");
231 | Console.WriteLine("Generates as JWT Bearer Grant and uses it against Maskinporten/ID-porten to get tokens.");
232 | Console.WriteLine();
233 | Console.WriteLine("Options:");
234 | p.WriteOptionDescriptions(Console.Out);
235 | }
236 |
237 | static void CheckParameters(OptionSet p)
238 | {
239 | var hasErrors = false;
240 |
241 | if (_certificateThumbPrint == null && _p12KeyStoreFile == null && _jwkFile == null || new [] { _certificateThumbPrint, _p12KeyStoreFile, _jwkFile }.Count(b => b != null) != 1)
242 | {
243 | Console.WriteLine("Requires exactly one of either --certificate_thumbprint or --keystore_path or --jwk_path");
244 | hasErrors = true;
245 | }
246 |
247 | if (_issuer == null)
248 | {
249 | Console.WriteLine("Requires --client_id");
250 | hasErrors = true;
251 | }
252 |
253 | if (_audience == null)
254 | {
255 | Console.WriteLine("Requires --audience");
256 | hasErrors = true;
257 | }
258 |
259 | if (_scopes == null)
260 | {
261 | Console.WriteLine("Requires --scopes");
262 | hasErrors = true;
263 | }
264 |
265 | if (_tokenEndpoint == null)
266 | {
267 | Console.WriteLine("Requires --token_endpoint");
268 | hasErrors = true;
269 | }
270 |
271 | if (!hasErrors) return;
272 | ShowHelp(p);
273 | Environment.Exit(1);
274 | }
275 |
276 | static string GetAuthorizeUrl(int serverPort, string codeChallenge)
277 | {
278 | var url = string.Format("{0}?scope={1}&acr_values=idporten-loa-substantial&client_id={2}&redirect_uri={3}&response_type=code&ui_locales=nb&code_challenge={4}&code_challenge_method=S256", _authorizeEndpoint, WebUtility.UrlEncode(_scopes), _issuer, WebUtility.UrlEncode(GetRedirectUri(serverPort)), codeChallenge);
279 | if (_resource != null)
280 | {
281 | url += "&resource=" + WebUtility.UrlEncode(_resource);
282 | }
283 |
284 | return url;
285 | }
286 |
287 | static string GenerateCodeVerifier()
288 | {
289 | int length = 64;
290 | const string availableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
291 | char[] chars = new char[length];
292 | Random random = new Random();
293 |
294 | for (int i = 0; i < length; i++)
295 | {
296 | chars[i] = availableChars[random.Next(0, availableChars.Length)];
297 | }
298 |
299 | return new string(chars);
300 | }
301 |
302 | public static string GeneratePkceChallenge(string text)
303 | {
304 | byte[] bytes = Encoding.ASCII.GetBytes(text);
305 | SHA256 hashString = SHA256.Create();
306 | byte[] hash = hashString.ComputeHash(bytes);
307 |
308 | string base64UrlHash = Convert.ToBase64String(hash);
309 | base64UrlHash = base64UrlHash.Replace('+', '-');
310 | base64UrlHash = base64UrlHash.Replace('/', '_');
311 | base64UrlHash = base64UrlHash.Split('=')[0];
312 |
313 | return base64UrlHash;
314 | }
315 |
316 | static string GetRedirectUri(int serverPort)
317 | {
318 | return "http://localhost:" + serverPort.ToString() + "/response";
319 | }
320 | }
321 | }
322 |
--------------------------------------------------------------------------------