├── 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 | --------------------------------------------------------------------------------