diff --git a/bridge/jira/client.go b/bridge/jira/client.go
index 15098a3c70da0e8c9706eea0f2f72727501df4b8..5e1db26fee1e4e6fd1f235540717ec73dd53bc30 100644
--- a/bridge/jira/client.go
+++ b/bridge/jira/client.go
@@ -14,10 +14,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/MichaelMure/git-bug/bridge/core"
-	"github.com/MichaelMure/git-bug/bug"
-	"github.com/MichaelMure/git-bug/input"
 	"github.com/pkg/errors"
+
+	"github.com/MichaelMure/git-bug/bug"
 )
 
 var errDone = errors.New("Iteration Done")
@@ -39,14 +38,14 @@ func ParseTime(timeStr string) (time.Time, error) {
 	return out, err
 }
 
-// MyTime is just a time.Time with a JSON serialization
-type MyTime struct {
+// Time is just a time.Time with a JSON serialization
+type Time struct {
 	time.Time
 }
 
 // UnmarshalJSON parses an RFC3339 date string into a time object
 // borrowed from: https://stackoverflow.com/a/39180230/141023
-func (self *MyTime) UnmarshalJSON(data []byte) (err error) {
+func (t *Time) UnmarshalJSON(data []byte) (err error) {
 	str := string(data)
 
 	// Get rid of the quotes "" around the value.
@@ -56,7 +55,7 @@ func (self *MyTime) UnmarshalJSON(data []byte) (err error) {
 	str = str[1 : len(str)-1]
 
 	timeObj, err := ParseTime(str)
-	self.Time = timeObj
+	t.Time = timeObj
 	return
 }
 
@@ -100,8 +99,8 @@ type Comment struct {
 	Body         string `json:"body"`
 	Author       User   `json:"author"`
 	UpdateAuthor User   `json:"updateAuthor"`
-	Created      MyTime `json:"created"`
-	Updated      MyTime `json:"updated"`
+	Created      Time   `json:"created"`
+	Updated      Time   `json:"updated"`
 }
 
 // CommentPage the JSON object holding a single page of comments returned
@@ -115,13 +114,13 @@ type CommentPage struct {
 }
 
 // NextStartAt return the index of the first item on the next page
-func (self *CommentPage) NextStartAt() int {
-	return self.StartAt + len(self.Comments)
+func (cp *CommentPage) NextStartAt() int {
+	return cp.StartAt + len(cp.Comments)
 }
 
 // IsLastPage return true if there are no more items beyond this page
-func (self *CommentPage) IsLastPage() bool {
-	return self.NextStartAt() >= self.Total
+func (cp *CommentPage) IsLastPage() bool {
+	return cp.NextStartAt() >= cp.Total
 }
 
 // IssueFields the JSON object returned as the "fields" member of an issue.
@@ -130,7 +129,7 @@ func (self *CommentPage) IsLastPage() bool {
 // https://docs.atlassian.com/software/jira/docs/api/REST/8.2.6/#api/2/issue-getIssue
 type IssueFields struct {
 	Creator     User        `json:"creator"`
-	Created     MyTime      `json:"created"`
+	Created     Time        `json:"created"`
 	Description string      `json:"description"`
 	Summary     string      `json:"summary"`
 	Comments    CommentPage `json:"comment"`
@@ -155,7 +154,7 @@ type ChangeLogItem struct {
 type ChangeLogEntry struct {
 	ID      string          `json:"id"`
 	Author  User            `json:"author"`
-	Created MyTime          `json:"created"`
+	Created Time            `json:"created"`
 	Items   []ChangeLogItem `json:"items"`
 }
 
@@ -171,16 +170,16 @@ type ChangeLogPage struct {
 }
 
 // NextStartAt return the index of the first item on the next page
-func (self *ChangeLogPage) NextStartAt() int {
-	return self.StartAt + len(self.Entries)
+func (clp *ChangeLogPage) NextStartAt() int {
+	return clp.StartAt + len(clp.Entries)
 }
 
 // IsLastPage return true if there are no more items beyond this page
-func (self *ChangeLogPage) IsLastPage() bool {
+func (clp *ChangeLogPage) IsLastPage() bool {
 	// NOTE(josh): The "isLast" field is returned on JIRA cloud, but not on
 	// JIRA server. If we can distinguish which one we are working with, we can
 	// possibly rely on that instead.
-	return self.NextStartAt() >= self.Total
+	return clp.NextStartAt() >= clp.Total
 }
 
 // Issue Top-level object for an issue
@@ -202,13 +201,13 @@ type SearchResult struct {
 }
 
 // NextStartAt return the index of the first item on the next page
-func (self *SearchResult) NextStartAt() int {
-	return self.StartAt + len(self.Issues)
+func (sr *SearchResult) NextStartAt() int {
+	return sr.StartAt + len(sr.Issues)
 }
 
 // IsLastPage return true if there are no more items beyond this page
-func (self *SearchResult) IsLastPage() bool {
-	return self.NextStartAt() >= self.Total
+func (sr *SearchResult) IsLastPage() bool {
+	return sr.NextStartAt() >= sr.Total
 }
 
 // SearchRequest the JSON object POSTed to the /search endpoint
@@ -296,8 +295,8 @@ type ServerInfo struct {
 	Version          string `json:"version"`
 	VersionNumbers   []int  `json:"versionNumbers"`
 	BuildNumber      int    `json:"buildNumber"`
-	BuildDate        MyTime `json:"buildDate"`
-	ServerTime       MyTime `json:"serverTime"`
+	BuildDate        Time   `json:"buildDate"`
+	ServerTime       Time   `json:"serverTime"`
 	ScmInfo          string `json:"scmInfo"`
 	BuildPartnerName string `json:"buildPartnerName"`
 	ServerTitle      string `json:"serverTitle"`
@@ -331,7 +330,7 @@ func (ct *ClientTransport) SetCredentials(username string, token string) {
 }
 
 // Client Thin wrapper around the http.Client providing jira-specific methods
-// for APIendpoints
+// for API endpoints
 type Client struct {
 	*http.Client
 	serverURL string
@@ -340,7 +339,7 @@ type Client struct {
 
 // NewClient Construct a new client connected to the provided server and
 // utilizing the given context for asynchronous events
-func NewClient(serverURL string, ctx context.Context) *Client {
+func NewClient(ctx context.Context, serverURL string) *Client {
 	cookiJar, _ := cookiejar.New(nil)
 	client := &http.Client{
 		Transport: &ClientTransport{underlyingTransport: http.DefaultTransport},
@@ -350,57 +349,20 @@ func NewClient(serverURL string, ctx context.Context) *Client {
 	return &Client{client, serverURL, ctx}
 }
 
-// Login POST credentials to the /session endpoing and get a session cookie
-func (client *Client) Login(conf core.Configuration) error {
-	credType := conf[keyCredentialsType]
-
-	if conf[keyCredentialsFile] != "" {
-		content, err := ioutil.ReadFile(conf[keyCredentialsFile])
-		if err != nil {
-			return err
-		}
-
-		switch credType {
-		case "SESSION":
-			return client.RefreshSessionTokenRaw(content)
-		case "TOKEN":
-			var params SessionQuery
-			err := json.Unmarshal(content, &params)
-			if err != nil {
-				return err
-			}
-			return client.SetTokenCredentials(params.Username, params.Password)
-		}
-		return fmt.Errorf("Unexpected credType: %s", credType)
-	}
-
-	username := conf[keyUsername]
-	if username == "" {
-		return fmt.Errorf(
-			"Invalid configuration lacks both a username and credentials sidecar " +
-				"path. At least one is required.")
-	}
-
-	password := conf[keyPassword]
-	if password == "" {
-		var err error
-		password, err = input.PromptPassword("Password", "password", input.Required)
-		if err != nil {
-			return err
-		}
-	}
-
+// Login POST credentials to the /session endpoint and get a session cookie
+func (client *Client) Login(credType, login, password string) error {
 	switch credType {
 	case "SESSION":
-		return client.RefreshSessionToken(username, password)
+		return client.RefreshSessionToken(login, password)
 	case "TOKEN":
-		return client.SetTokenCredentials(username, password)
+		return client.SetTokenCredentials(login, password)
+	default:
+		panic("unknown Jira cred type")
 	}
-	return fmt.Errorf("Unexpected credType: %s", credType)
 }
 
 // RefreshSessionToken formulate the JSON request object from the user
-// credentials and POST it to the /session endpoing and get a session cookie
+// credentials and POST it to the /session endpoint and get a session cookie
 func (client *Client) RefreshSessionToken(username, password string) error {
 	params := SessionQuery{
 		Username: username,
@@ -415,7 +377,7 @@ func (client *Client) RefreshSessionToken(username, password string) error {
 	return client.RefreshSessionTokenRaw(data)
 }
 
-// SetTokenCredentials POST credentials to the /session endpoing and get a
+// SetTokenCredentials POST credentials to the /session endpoint and get a
 // session cookie
 func (client *Client) SetTokenCredentials(username, password string) error {
 	switch transport := client.Transport.(type) {
@@ -427,7 +389,7 @@ func (client *Client) SetTokenCredentials(username, password string) error {
 	return nil
 }
 
-// RefreshSessionTokenRaw POST credentials to the /session endpoing and get a
+// RefreshSessionTokenRaw POST credentials to the /session endpoint and get a
 // session cookie
 func (client *Client) RefreshSessionTokenRaw(credentialsJSON []byte) error {
 	postURL := fmt.Sprintf("%s/rest/auth/1/session", client.serverURL)
@@ -796,7 +758,7 @@ func (client *Client) IterComments(idOrKey string, pageSize int) *CommentIterato
 	return iter
 }
 
-// GetChangeLog fetchs one page of the changelog for an issue via the
+// GetChangeLog fetch one page of the changelog for an issue via the
 // /issue/{IssueIdOrKey}/changelog endpoint (for JIRA cloud) or
 // /issue/{IssueIdOrKey} with (fields=*none&expand=changelog)
 // (for JIRA server)
@@ -1489,8 +1451,8 @@ func (client *Client) GetServerInfo() (*ServerInfo, error) {
 }
 
 // GetServerTime returns the current time on the server
-func (client *Client) GetServerTime() (MyTime, error) {
-	var result MyTime
+func (client *Client) GetServerTime() (Time, error) {
+	var result Time
 	info, err := client.GetServerInfo()
 	if err != nil {
 		return result, err
diff --git a/bridge/jira/config.go b/bridge/jira/config.go
index 077f258a8f2ef407a166e81ff86b0e679ed7bf8e..db52b83d316bfaba83cfcd2bd82157e9a0440f76 100644
--- a/bridge/jira/config.go
+++ b/bridge/jira/config.go
@@ -1,15 +1,14 @@
 package jira
 
 import (
-	"encoding/json"
+	"context"
 	"fmt"
-	"io/ioutil"
-
-	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/repository"
 )
 
 const moreConfigText = `
@@ -28,32 +27,27 @@ after October 1st 2019 must use "TOKEN" authentication. You must create a user
 API token and the client will provide this along with your username with each
 request.`
 
-// Configure sets up the bridge configuration
-func (g *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
-	conf := make(core.Configuration)
-	conf[core.ConfigKeyTarget] = target
+func (*Jira) ValidParams() map[string]interface{} {
+	return map[string]interface{}{
+		"BaseURL":    nil,
+		"Login":      nil,
+		"CredPrefix": nil,
+		"Project":    nil,
+	}
+}
 
+// Configure sets up the bridge configuration
+func (j *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
 	var err error
 
-	// if params.Token != "" || params.TokenStdin {
-	// 	return nil, fmt.Errorf(
-	// 		"JIRA session tokens are extremely short lived. We don't store them " +
-	// 			"in the configuration, so they are not valid for this bridge.")
-	// }
-
-	if params.Owner != "" {
-		fmt.Println("warning: --owner is ineffective for a Jira bridge")
-	}
-
-	serverURL := params.URL
-	if serverURL == "" {
+	baseURL := params.BaseURL
+	if baseURL == "" {
 		// terminal prompt
-		serverURL, err = input.Prompt("JIRA server URL", "URL", input.Required)
+		baseURL, err = input.Prompt("JIRA server URL", "URL", input.Required, input.IsURL)
 		if err != nil {
 			return nil, err
 		}
 	}
-	conf[keyServer] = serverURL
 
 	project := params.Project
 	if project == "" {
@@ -62,77 +56,56 @@ func (g *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.
 			return nil, err
 		}
 	}
-	conf[keyProject] = project
 
 	fmt.Println(credTypeText)
-	credType, err := input.PromptChoice("Authentication mechanism", []string{"SESSION", "TOKEN"})
-	if err != nil {
-		return nil, err
-	}
-
-	switch credType {
-	case 1:
-		conf[keyCredentialsType] = "SESSION"
-	case 2:
-		conf[keyCredentialsType] = "TOKEN"
-	}
-
-	fmt.Println("How would you like to store your JIRA login credentials?")
-	credTargetChoice, err := input.PromptChoice("Credential storage", []string{
-		"sidecar JSON file: Your credentials will be stored in a JSON sidecar next" +
-			"to your git config. Note that it will contain your JIRA password in clear" +
-			"text.",
-		"git-config: Your credentials will be stored in the git config. Note that" +
-			"it will contain your JIRA password in clear text.",
-		"username in config, askpass: Your username will be stored in the git" +
-			"config. We will ask you for your password each time you execute the bridge.",
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	username, err := input.Prompt("JIRA username", "username", input.Required)
+	credTypeInput, err := input.PromptChoice("Authentication mechanism", []string{"SESSION", "TOKEN"})
 	if err != nil {
 		return nil, err
 	}
+	credType := []string{"SESSION", "TOKEN"}[credTypeInput]
 
-	password, err := input.PromptPassword("Password", "password", input.Required)
-	if err != nil {
-		return nil, err
-	}
+	var login string
+	var cred auth.Credential
 
-	switch credTargetChoice {
-	case 1:
-		// TODO: a validator to see if the path is writable ?
-		credentialsFile, err := input.Prompt("Credentials file path", "path", input.Required)
+	switch {
+	case params.CredPrefix != "":
+		cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
 		if err != nil {
 			return nil, err
 		}
-		conf[keyCredentialsFile] = credentialsFile
-		jsonData, err := json.Marshal(&SessionQuery{Username: username, Password: password})
-		if err != nil {
-			return nil, err
+		l, ok := cred.GetMetadata(auth.MetaKeyLogin)
+		if !ok {
+			return nil, fmt.Errorf("credential doesn't have a login")
+		}
+		login = l
+	default:
+		login := params.Login
+		if login == "" {
+			// TODO: validate username
+			login, err = input.Prompt("JIRA login", "login", input.Required)
+			if err != nil {
+				return nil, err
+			}
 		}
-		err = ioutil.WriteFile(credentialsFile, jsonData, 0644)
+		cred, err = promptCredOptions(repo, login, baseURL)
 		if err != nil {
-			return nil, errors.Wrap(
-				err, fmt.Sprintf("Unable to write credentials to %s", credentialsFile))
+			return nil, err
 		}
-	case 2:
-		conf[keyUsername] = username
-		conf[keyPassword] = password
-	case 3:
-		conf[keyUsername] = username
 	}
 
-	err = g.ValidateConfig(conf)
+	conf := make(core.Configuration)
+	conf[core.ConfigKeyTarget] = target
+	conf[confKeyBaseUrl] = baseURL
+	conf[confKeyProject] = project
+	conf[confKeyCredentialType] = credType
+
+	err = j.ValidateConfig(conf)
 	if err != nil {
 		return nil, err
 	}
 
 	fmt.Printf("Attempting to login with credentials...\n")
-	client := NewClient(serverURL, nil)
-	err = client.Login(conf)
+	client, err := buildClient(context.TODO(), baseURL, credType, cred)
 	if err != nil {
 		return nil, err
 	}
@@ -144,7 +117,12 @@ func (g *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.
 		return nil, fmt.Errorf(
 			"Project %s doesn't exist on %s, or authentication credentials for (%s)"+
 				" are invalid",
-			project, serverURL, username)
+			project, baseURL, login)
+	}
+
+	err = core.FinishConfig(repo, metaKeyJiraLogin, login)
+	if err != nil {
+		return nil, err
 	}
 
 	fmt.Print(moreConfigText)
@@ -159,9 +137,46 @@ func (*Jira) ValidateConfig(conf core.Configuration) error {
 		return fmt.Errorf("unexpected target name: %v", v)
 	}
 
-	if _, ok := conf[keyProject]; !ok {
-		return fmt.Errorf("missing %s key", keyProject)
+	if _, ok := conf[confKeyProject]; !ok {
+		return fmt.Errorf("missing %s key", confKeyProject)
 	}
 
 	return nil
 }
+
+func promptCredOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
+	creds, err := auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithKind(auth.KindToken),
+		auth.WithMeta(auth.MetaKeyLogin, login),
+		auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	cred, index, err := input.PromptCredential(target, "password", creds, []string{
+		"enter my password",
+		"ask my password each time",
+	})
+	switch {
+	case err != nil:
+		return nil, err
+	case cred != nil:
+		return cred, nil
+	case index == 0:
+		password, err := input.PromptPassword("Password", "password", input.Required)
+		if err != nil {
+			return nil, err
+		}
+		lp := auth.NewLoginPassword(target, login, password)
+		lp.SetMetadata(auth.MetaKeyLogin, login)
+		return lp, nil
+	case index == 1:
+		l := auth.NewLogin(target, login)
+		l.SetMetadata(auth.MetaKeyLogin, login)
+		return l, nil
+	default:
+		panic("missed case")
+	}
+}
diff --git a/bridge/jira/export.go b/bridge/jira/export.go
index f329e4904d3dca2f3c962936be654381a8c8cc7d..3706626349299437afaa69c973e931f736559df1 100644
--- a/bridge/jira/export.go
+++ b/bridge/jira/export.go
@@ -5,14 +5,17 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"os"
 	"time"
 
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/identity"
 )
 
 var (
@@ -23,14 +26,12 @@ var (
 type jiraExporter struct {
 	conf core.Configuration
 
-	// the current user identity
-	// NOTE: this is only needed to mock the credentials database in
-	// getIdentityClient
-	userIdentity entity.Id
-
 	// cache identities clients
 	identityClient map[entity.Id]*Client
 
+	// the mapping from git-bug "status" to JIRA "status" id
+	statusMap map[string]string
+
 	// cache identifiers used to speed up exporting operations
 	// cleared for each bug
 	cachedOperationIDs map[entity.Id]string
@@ -43,62 +44,99 @@ type jiraExporter struct {
 }
 
 // Init .
-func (je *jiraExporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
+func (je *jiraExporter) Init(ctx context.Context, repo *cache.RepoCache, conf core.Configuration) error {
 	je.conf = conf
 	je.identityClient = make(map[entity.Id]*Client)
 	je.cachedOperationIDs = make(map[entity.Id]string)
 	je.cachedLabels = make(map[string]string)
-	return nil
-}
 
-// getIdentityClient return an API client configured with the credentials
-// of the given identity. If no client were found it will initialize it from
-// the known credentials map and cache it for next use
-func (je *jiraExporter) getIdentityClient(ctx context.Context, id entity.Id) (*Client, error) {
-	client, ok := je.identityClient[id]
-	if ok {
-		return client, nil
+	statusMap, err := getStatusMap(je.conf)
+	if err != nil {
+		return err
+	}
+	je.statusMap = statusMap
+
+	// preload all clients
+	err = je.cacheAllClient(ctx, repo)
+	if err != nil {
+		return err
 	}
 
-	client = NewClient(je.conf[keyServer], ctx)
+	if len(je.identityClient) == 0 {
+		return fmt.Errorf("no credentials for this bridge")
+	}
+
+	var client *Client
+	for _, c := range je.identityClient {
+		client = c
+		break
+	}
 
-	// NOTE: as a future enhancement, the bridge would ideally be able to generate
-	// a separate session token for each user that we have stored credentials
-	// for. However we currently only support a single user.
-	if id != je.userIdentity {
-		return nil, ErrMissingCredentials
+	if client == nil {
+		panic("nil client")
 	}
-	err := client.Login(je.conf)
+
+	je.project, err = client.GetProject(je.conf[confKeyProject])
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	je.identityClient[id] = client
-	return client, nil
+	return nil
 }
 
-// ExportAll export all event made by the current user to Jira
-func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
-	out := make(chan core.ExportResult)
-
-	user, err := repo.GetUserIdentity()
+func (je *jiraExporter) cacheAllClient(ctx context.Context, repo *cache.RepoCache) error {
+	creds, err := auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithKind(auth.KindLoginPassword), auth.WithKind(auth.KindLogin),
+		auth.WithMeta(auth.MetaKeyBaseURL, je.conf[confKeyBaseUrl]),
+	)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	// NOTE: this is currently only need to mock the credentials database in
-	// getIdentityClient.
-	je.userIdentity = user.Id()
-	client, err := je.getIdentityClient(ctx, user.Id())
-	if err != nil {
-		return nil, err
+	for _, cred := range creds {
+		login, ok := cred.GetMetadata(auth.MetaKeyLogin)
+		if !ok {
+			_, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Jira login\n", cred.ID().Human())
+			continue
+		}
+
+		user, err := repo.ResolveIdentityImmutableMetadata(metaKeyJiraLogin, login)
+		if err == identity.ErrIdentityNotExist {
+			continue
+		}
+		if err != nil {
+			return nil
+		}
+
+		if _, ok := je.identityClient[user.Id()]; !ok {
+			client, err := buildClient(ctx, je.conf[confKeyBaseUrl], je.conf[confKeyCredentialType], creds[0])
+			if err != nil {
+				return err
+			}
+			je.identityClient[user.Id()] = client
+		}
 	}
 
-	je.project, err = client.GetProject(je.conf[keyProject])
-	if err != nil {
-		return nil, err
+	return nil
+}
+
+// getClientForIdentity return an API client configured with the credentials
+// of the given identity. If no client were found it will initialize it from
+// the known credentials and cache it for next use.
+func (je *jiraExporter) getClientForIdentity(userId entity.Id) (*Client, error) {
+	client, ok := je.identityClient[userId]
+	if ok {
+		return client, nil
 	}
 
+	return nil, ErrMissingCredentials
+}
+
+// ExportAll export all event made by the current user to Jira
+func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
+	out := make(chan core.ExportResult)
+
 	go func() {
 		defer close(out)
 
@@ -134,7 +172,7 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si
 
 				if snapshot.HasAnyActor(allIdentitiesIds...) {
 					// try to export the bug and it associated events
-					err := je.exportBug(ctx, b, since, out)
+					err := je.exportBug(ctx, b, out)
 					if err != nil {
 						out <- core.NewExportError(errors.Wrap(err, "can't export bug"), id)
 						return
@@ -150,7 +188,7 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si
 }
 
 // exportBug publish bugs and related events
-func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since time.Time, out chan<- core.ExportResult) error {
+func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out chan<- core.ExportResult) error {
 	snapshot := b.Snapshot()
 
 	var bugJiraID string
@@ -174,7 +212,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 
 	// skip bug if it is a jira bug but is associated with another project
 	// (one bridge per JIRA project)
-	project, ok := snapshot.GetCreateMetadata(keyJiraProject)
+	project, ok := snapshot.GetCreateMetadata(metaKeyJiraProject)
 	if ok && !stringInSlice(project, []string{je.project.ID, je.project.Key}) {
 		out <- core.NewExportNothing(
 			b.Id(), fmt.Sprintf("issue tagged with project: %s", project))
@@ -182,18 +220,18 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 	}
 
 	// get jira bug ID
-	jiraID, ok := snapshot.GetCreateMetadata(keyJiraID)
+	jiraID, ok := snapshot.GetCreateMetadata(metaKeyJiraId)
 	if ok {
 		// will be used to mark operation related to a bug as exported
 		bugJiraID = jiraID
 	} else {
 		// check that we have credentials for operation author
-		client, err := je.getIdentityClient(ctx, author.Id())
+		client, err := je.getClientForIdentity(author.Id())
 		if err != nil {
 			// if bug is not yet exported and we do not have the author's credentials
 			// then there is nothing we can do, so just skip this bug
 			out <- core.NewExportNothing(
-				b.Id(), fmt.Sprintf("missing author token for user %.8s",
+				b.Id(), fmt.Sprintf("missing author credentials for user %.8s",
 					author.Id().String()))
 			return err
 		}
@@ -201,7 +239,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 		// Load any custom fields required to create an issue from the git
 		// config file.
 		fields := make(map[string]interface{})
-		defaultFields, hasConf := je.conf[keyCreateDefaults]
+		defaultFields, hasConf := je.conf[confKeyCreateDefaults]
 		if hasConf {
 			err = json.Unmarshal([]byte(defaultFields), &fields)
 			if err != nil {
@@ -215,7 +253,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 				"id": "10001",
 			}
 		}
-		bugIDField, hasConf := je.conf[keyCreateGitBug]
+		bugIDField, hasConf := je.conf[confKeyCreateGitBug]
 		if hasConf {
 			// If the git configuration also indicates it, we can assign the git-bug
 			// id to a custom field to assist in integrations
@@ -257,12 +295,6 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 	// cache operation jira id
 	je.cachedOperationIDs[createOp.Id()] = bugJiraID
 
-	// lookup the mapping from git-bug "status" to JIRA "status" id
-	statusMap, err := getStatusMap(je.conf)
-	if err != nil {
-		return err
-	}
-
 	for _, op := range snapshot.Operations[1:] {
 		// ignore SetMetadata operations
 		if _, ok := op.(*bug.SetMetadataOperation); ok {
@@ -272,13 +304,13 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 		// ignore operations already existing in jira (due to import or export)
 		// cache the ID of already exported or imported issues and events from
 		// Jira
-		if id, ok := op.GetMetadata(keyJiraID); ok {
+		if id, ok := op.GetMetadata(metaKeyJiraId); ok {
 			je.cachedOperationIDs[op.Id()] = id
 			continue
 		}
 
 		opAuthor := op.GetAuthor()
-		client, err := je.getIdentityClient(ctx, opAuthor.Id())
+		client, err := je.getClientForIdentity(opAuthor.Id())
 		if err != nil {
 			out <- core.NewExportError(
 				fmt.Errorf("missing operation author credentials for user %.8s",
@@ -340,7 +372,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 			}
 
 		case *bug.SetStatusOperation:
-			jiraStatus, hasStatus := statusMap[opr.Status.String()]
+			jiraStatus, hasStatus := je.statusMap[opr.Status.String()]
 			if hasStatus {
 				exportTime, err = UpdateIssueStatus(client, bugJiraID, jiraStatus)
 				if err != nil {
@@ -407,11 +439,11 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
 
 func markOperationAsExported(b *cache.BugCache, target entity.Id, jiraID, jiraProject string, exportTime time.Time) error {
 	newMetadata := map[string]string{
-		keyJiraID:      jiraID,
-		keyJiraProject: jiraProject,
+		metaKeyJiraId:      jiraID,
+		metaKeyJiraProject: jiraProject,
 	}
 	if !exportTime.IsZero() {
-		newMetadata[keyJiraExportTime] = exportTime.Format(http.TimeFormat)
+		newMetadata[metaKeyJiraExportTime] = exportTime.Format(http.TimeFormat)
 	}
 
 	_, err := b.SetMetadata(target, newMetadata)
diff --git a/bridge/jira/import.go b/bridge/jira/import.go
index 6a755a36c6bf342620051e8f21c32029f9c6c4fa..bfe83f4d92028263840f7d7b9222e9307d54db95 100644
--- a/bridge/jira/import.go
+++ b/bridge/jira/import.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
@@ -17,51 +18,75 @@ import (
 )
 
 const (
-	keyJiraID          = "jira-id"
-	keyJiraOperationID = "jira-derived-id"
-	keyJiraKey         = "jira-key"
-	keyJiraUser        = "jira-user"
-	keyJiraProject     = "jira-project"
-	keyJiraExportTime  = "jira-export-time"
-	defaultPageSize    = 10
+	defaultPageSize = 10
 )
 
 // jiraImporter implement the Importer interface
 type jiraImporter struct {
 	conf core.Configuration
 
+	client *Client
+
 	// send only channel
 	out chan<- core.ImportResult
 }
 
 // Init .
-func (ji *jiraImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
+func (ji *jiraImporter) Init(ctx context.Context, repo *cache.RepoCache, conf core.Configuration) error {
 	ji.conf = conf
-	return nil
+
+	var cred auth.Credential
+
+	// Prioritize LoginPassword credentials to avoid a prompt
+	creds, err := auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyBaseUrl]),
+		auth.WithKind(auth.KindLoginPassword),
+	)
+	if err != nil {
+		return err
+	}
+	if len(creds) > 0 {
+		cred = creds[0]
+		goto end
+	}
+
+	creds, err = auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyBaseUrl]),
+		auth.WithKind(auth.KindLogin),
+	)
+	if err != nil {
+		return err
+	}
+	if len(creds) > 0 {
+		cred = creds[0]
+	}
+
+end:
+	if cred == nil {
+		return fmt.Errorf("no credential for this bridge")
+	}
+
+	// TODO(josh)[da52062]: Validate token and if it is expired then prompt for
+	// credentials and generate a new one
+	ji.client, err = buildClient(ctx, conf[confKeyBaseUrl], conf[confKeyCredentialType], cred)
+	return err
 }
 
 // ImportAll iterate over all the configured repository issues and ensure the
 // creation of the missing issues / timeline items / edits / label events ...
 func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
 	sinceStr := since.Format("2006-01-02 15:04")
-	serverURL := ji.conf[keyServer]
-	project := ji.conf[keyProject]
-	// TODO(josh)[da52062]: Validate token and if it is expired then prompt for
-	// credentials and generate a new one
+	project := ji.conf[confKeyProject]
+
 	out := make(chan core.ImportResult)
 	ji.out = out
 
 	go func() {
 		defer close(ji.out)
 
-		client := NewClient(serverURL, ctx)
-		err := client.Login(ji.conf)
-		if err != nil {
-			out <- core.NewImportError(err, "")
-			return
-		}
-
-		message, err := client.Search(
+		message, err := ji.client.Search(
 			fmt.Sprintf("project=%s AND updatedDate>\"%s\"", project, sinceStr), 0, 0)
 		if err != nil {
 			out <- core.NewImportError(err, "")
@@ -73,7 +98,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
 		jql := fmt.Sprintf("project=%s AND updatedDate>\"%s\"", project, sinceStr)
 		var searchIter *SearchIterator
 		for searchIter =
-			client.IterSearch(jql, defaultPageSize); searchIter.HasNext(); {
+			ji.client.IterSearch(jql, defaultPageSize); searchIter.HasNext(); {
 			issue := searchIter.Next()
 			b, err := ji.ensureIssue(repo, *issue)
 			if err != nil {
@@ -84,7 +109,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
 
 			var commentIter *CommentIterator
 			for commentIter =
-				client.IterComments(issue.ID, defaultPageSize); commentIter.HasNext(); {
+				ji.client.IterComments(issue.ID, defaultPageSize); commentIter.HasNext(); {
 				comment := commentIter.Next()
 				err := ji.ensureComment(repo, b, *comment)
 				if err != nil {
@@ -100,7 +125,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
 
 			var changelogIter *ChangeLogIterator
 			for changelogIter =
-				client.IterChangeLog(issue.ID, defaultPageSize); changelogIter.HasNext(); {
+				ji.client.IterChangeLog(issue.ID, defaultPageSize); changelogIter.HasNext(); {
 				changelogEntry := changelogIter.Next()
 
 				// Advance the operation iterator up to the first operation which has
@@ -110,7 +135,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
 				var exportTime time.Time
 				for ; opIdx < len(snapshot.Operations); opIdx++ {
 					exportTimeStr, hasTime := snapshot.Operations[opIdx].GetMetadata(
-						keyJiraExportTime)
+						metaKeyJiraExportTime)
 					if !hasTime {
 						continue
 					}
@@ -156,7 +181,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
 func (ji *jiraImporter) ensurePerson(repo *cache.RepoCache, user User) (*cache.IdentityCache, error) {
 	// Look first in the cache
 	i, err := repo.ResolveIdentityImmutableMetadata(
-		keyJiraUser, string(user.Key))
+		metaKeyJiraUser, string(user.Key))
 	if err == nil {
 		return i, nil
 	}
@@ -169,7 +194,7 @@ func (ji *jiraImporter) ensurePerson(repo *cache.RepoCache, user User) (*cache.I
 		user.EmailAddress,
 		user.Key,
 		map[string]string{
-			keyJiraUser: string(user.Key),
+			metaKeyJiraUser: string(user.Key),
 		},
 	)
 
@@ -188,7 +213,7 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugCreateMetadata(keyJiraID, issue.ID)
+	b, err := repo.ResolveBugCreateMetadata(metaKeyJiraId, issue.ID)
 	if err != nil && err != bug.ErrBugNotExist {
 		return nil, err
 	}
@@ -210,9 +235,9 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
 			nil,
 			map[string]string{
 				core.MetaKeyOrigin: target,
-				keyJiraID:          issue.ID,
-				keyJiraKey:         issue.Key,
-				keyJiraProject:     ji.conf[keyProject],
+				metaKeyJiraId:      issue.ID,
+				metaKeyJiraKey:     issue.Key,
+				metaKeyJiraProject: ji.conf[confKeyProject],
 			})
 		if err != nil {
 			return nil, err
@@ -225,7 +250,7 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
 }
 
 // Return a unique string derived from a unique jira id and a timestamp
-func getTimeDerivedID(jiraID string, timestamp MyTime) string {
+func getTimeDerivedID(jiraID string, timestamp Time) string {
 	return fmt.Sprintf("%s-%d", jiraID, timestamp.Unix())
 }
 
@@ -238,7 +263,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 	}
 
 	targetOpID, err := b.ResolveOperationWithMetadata(
-		keyJiraID, item.ID)
+		metaKeyJiraId, item.ID)
 	if err != nil && err != cache.ErrNoMatchingOp {
 		return err
 	}
@@ -263,7 +288,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 			cleanText,
 			nil,
 			map[string]string{
-				keyJiraID: item.ID,
+				metaKeyJiraId: item.ID,
 			},
 		)
 		if err != nil {
@@ -284,7 +309,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 	// timestamp. Note that this must be consistent with the exporter during
 	// export of an EditCommentOperation
 	derivedID := getTimeDerivedID(item.ID, item.Updated)
-	_, err = b.ResolveOperationWithMetadata(keyJiraID, derivedID)
+	_, err = b.ResolveOperationWithMetadata(metaKeyJiraId, derivedID)
 	if err == nil {
 		// Already imported this edition
 		return nil
@@ -311,7 +336,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 		targetOpID,
 		cleanText,
 		map[string]string{
-			keyJiraID: derivedID,
+			metaKeyJiraId: derivedID,
 		},
 	)
 
@@ -358,7 +383,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 	// If we have an operation which is already mapped to the entire changelog
 	// entry then that means this changelog entry was induced by an export
 	// operation and we've already done the match, so we skip this one
-	_, err := b.ResolveOperationWithMetadata(keyJiraOperationID, entry.ID)
+	_, err := b.ResolveOperationWithMetadata(metaKeyJiraOperationId, entry.ID)
 	if err == nil {
 		return nil
 	} else if err != cache.ErrNoMatchingOp {
@@ -400,7 +425,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			opr, isRightType := potentialOp.(*bug.LabelChangeOperation)
 			if isRightType && labelSetsMatch(addedLabels, opr.Added) && labelSetsMatch(removedLabels, opr.Removed) {
 				_, err := b.SetMetadata(opr.Id(), map[string]string{
-					keyJiraOperationID: entry.ID,
+					metaKeyJiraOperationId: entry.ID,
 				})
 				if err != nil {
 					return err
@@ -412,7 +437,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			opr, isRightType := potentialOp.(*bug.SetStatusOperation)
 			if isRightType && statusMap[opr.Status.String()] == item.To {
 				_, err := b.SetMetadata(opr.Id(), map[string]string{
-					keyJiraOperationID: entry.ID,
+					metaKeyJiraOperationId: entry.ID,
 				})
 				if err != nil {
 					return err
@@ -426,7 +451,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			opr, isRightType := potentialOp.(*bug.SetTitleOperation)
 			if isRightType && opr.Title == item.To {
 				_, err := b.SetMetadata(opr.Id(), map[string]string{
-					keyJiraOperationID: entry.ID,
+					metaKeyJiraOperationId: entry.ID,
 				})
 				if err != nil {
 					return err
@@ -442,7 +467,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 				opr.Target == b.Snapshot().Operations[0].Id() &&
 				opr.Message == item.ToString {
 				_, err := b.SetMetadata(opr.Id(), map[string]string{
-					keyJiraOperationID: entry.ID,
+					metaKeyJiraOperationId: entry.ID,
 				})
 				if err != nil {
 					return err
@@ -457,7 +482,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 	// changelog entry item as a separate git-bug operation.
 	for idx, item := range entry.Items {
 		derivedID := getIndexDerivedID(entry.ID, idx)
-		_, err := b.ResolveOperationWithMetadata(keyJiraOperationID, derivedID)
+		_, err := b.ResolveOperationWithMetadata(metaKeyJiraOperationId, derivedID)
 		if err == nil {
 			continue
 		}
@@ -477,8 +502,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 				addedLabels,
 				removedLabels,
 				map[string]string{
-					keyJiraID:          entry.ID,
-					keyJiraOperationID: derivedID,
+					metaKeyJiraId:          entry.ID,
+					metaKeyJiraOperationId: derivedID,
 				},
 			)
 			if err != nil {
@@ -496,8 +521,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 						author,
 						entry.Created.Unix(),
 						map[string]string{
-							keyJiraID:          entry.ID,
-							keyJiraOperationID: derivedID,
+							metaKeyJiraId:          entry.ID,
+							metaKeyJiraOperationId: derivedID,
 						},
 					)
 					if err != nil {
@@ -510,8 +535,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 						author,
 						entry.Created.Unix(),
 						map[string]string{
-							keyJiraID:          entry.ID,
-							keyJiraOperationID: derivedID,
+							metaKeyJiraId:          entry.ID,
+							metaKeyJiraOperationId: derivedID,
 						},
 					)
 					if err != nil {
@@ -534,8 +559,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 				entry.Created.Unix(),
 				string(item.ToString),
 				map[string]string{
-					keyJiraID:          entry.ID,
-					keyJiraOperationID: derivedID,
+					metaKeyJiraId:          entry.ID,
+					metaKeyJiraOperationId: derivedID,
 				},
 			)
 			if err != nil {
@@ -552,8 +577,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 				entry.Created.Unix(),
 				string(item.ToString),
 				map[string]string{
-					keyJiraID:          entry.ID,
-					keyJiraOperationID: derivedID,
+					metaKeyJiraId:          entry.ID,
+					metaKeyJiraOperationId: derivedID,
 				},
 			)
 			if err != nil {
@@ -580,7 +605,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 }
 
 func getStatusMap(conf core.Configuration) (map[string]string, error) {
-	mapStr, hasConf := conf[keyIDMap]
+	mapStr, hasConf := conf[confKeyIDMap]
 	if !hasConf {
 		return map[string]string{
 			bug.OpenStatus.String():   "1",
@@ -604,7 +629,7 @@ func getStatusMapReverse(conf core.Configuration) (map[string]string, error) {
 		outMap[val] = key
 	}
 
-	mapStr, hasConf := conf[keyIDRevMap]
+	mapStr, hasConf := conf[confKeyIDRevMap]
 	if !hasConf {
 		return outMap, nil
 	}
diff --git a/bridge/jira/jira.go b/bridge/jira/jira.go
index 43a11c05ed987912681127f20af1c447fcb14148..0ba27df39bf9effc6d1f41539745cb3d085b50c8 100644
--- a/bridge/jira/jira.go
+++ b/bridge/jira/jira.go
@@ -2,27 +2,36 @@
 package jira
 
 import (
+	"context"
+	"fmt"
 	"sort"
 	"time"
 
 	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/input"
 )
 
 const (
 	target = "jira"
 
-	metaKeyJiraLogin = "jira-login"
-
-	keyServer          = "server"
-	keyProject         = "project"
-	keyCredentialsType = "credentials-type"
-	keyCredentialsFile = "credentials-file"
-	keyUsername        = "username"
-	keyPassword        = "password"
-	keyIDMap           = "bug-id-map"
-	keyIDRevMap        = "bug-id-revmap"
-	keyCreateDefaults  = "create-issue-defaults"
-	keyCreateGitBug    = "create-issue-gitbug-id"
+	metaKeyJiraId          = "jira-id"
+	metaKeyJiraOperationId = "jira-derived-id"
+	metaKeyJiraKey         = "jira-key"
+	metaKeyJiraUser        = "jira-user"
+	metaKeyJiraProject     = "jira-project"
+	metaKeyJiraExportTime  = "jira-export-time"
+	metaKeyJiraLogin       = "jira-login"
+
+	confKeyBaseUrl        = "base-url"
+	confKeyProject        = "project"
+	confKeyCredentialType = "credentials-type" // "SESSION" or "TOKEN"
+	confKeyIDMap          = "bug-id-map"
+	confKeyIDRevMap       = "bug-id-revmap"
+	// the issue type when exporting a new bug. Default is Story (10001)
+	confKeyCreateDefaults = "create-issue-defaults"
+	// if set, the bridge fill this JIRA field with the `git-bug` id when exporting
+	confKeyCreateGitBug = "create-issue-gitbug-id"
 
 	defaultTimeout = 60 * time.Second
 )
@@ -51,6 +60,32 @@ func (*Jira) NewExporter() core.Exporter {
 	return &jiraExporter{}
 }
 
+func buildClient(ctx context.Context, baseURL string, credType string, cred auth.Credential) (*Client, error) {
+	client := NewClient(ctx, baseURL)
+
+	var login, password string
+
+	switch cred := cred.(type) {
+	case *auth.LoginPassword:
+		login = cred.Login
+		password = cred.Password
+	case *auth.Login:
+		login = cred.Login
+		p, err := input.PromptPassword(fmt.Sprintf("Password for %s", login), "password", input.Required)
+		if err != nil {
+			return nil, err
+		}
+		password = p
+	}
+
+	err := client.Login(credType, login, password)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
 // stringInSlice returns true if needle is found in haystack
 func stringInSlice(needle string, haystack []string) bool {
 	for _, match := range haystack {